diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 2b2a31b711..8aa65b66f1 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -2343,6 +2343,31 @@ export interface SearchResponseDto { */ 'assets': SearchAssetResponseDto; } +/** + * + * @export + * @interface ServerConfigDto + */ +export interface ServerConfigDto { + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'loginPageMessage': string; + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'mapTileUrl': string; + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'oauthButtonText': string; +} /** * * @export @@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto { * @memberof ServerFeaturesDto */ 'facialRecognition': boolean; + /** + * + * @type {boolean} + * @memberof ServerFeaturesDto + */ + 'map': boolean; /** * * @type {boolean} @@ -2810,6 +2841,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'machineLearning': SystemConfigMachineLearningDto; + /** + * + * @type {SystemConfigMapDto} + * @memberof SystemConfigDto + */ + 'map': SystemConfigMapDto; /** * * @type {SystemConfigOAuthDto} @@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto { */ 'url': string; } +/** + * + * @export + * @interface SystemConfigMapDto + */ +export interface SystemConfigMapDto { + /** + * + * @type {boolean} + * @memberof SystemConfigMapDto + */ + 'enabled': boolean; + /** + * + * @type {string} + * @memberof SystemConfigMapDto + */ + 'tileUrl': string; +} /** * * @export @@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI { */ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getServerConfig: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/server-info/config`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. @@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur export const ServerInfoApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getServerConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getServerConfig(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) { export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = ServerInfoApiFp(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getServerConfig(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getServerConfig(options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas * @extends {BaseAPI} */ export class ServerInfoApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ServerInfoApi + */ + public getServerConfig(options?: AxiosRequestConfig) { + return ServerInfoApiFp(this.configuration).getServerConfig(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 5cb7fa6515..40490a32de 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -97,6 +97,7 @@ doc/SearchExploreResponseDto.md doc/SearchFacetCountResponseDto.md doc/SearchFacetResponseDto.md doc/SearchResponseDto.md +doc/ServerConfigDto.md doc/ServerFeaturesDto.md doc/ServerInfoApi.md doc/ServerInfoResponseDto.md @@ -116,6 +117,7 @@ doc/SystemConfigDto.md doc/SystemConfigFFmpegDto.md doc/SystemConfigJobDto.md doc/SystemConfigMachineLearningDto.md +doc/SystemConfigMapDto.md doc/SystemConfigOAuthDto.md doc/SystemConfigPasswordLoginDto.md doc/SystemConfigStorageTemplateDto.md @@ -249,6 +251,7 @@ lib/model/search_explore_response_dto.dart lib/model/search_facet_count_response_dto.dart lib/model/search_facet_response_dto.dart lib/model/search_response_dto.dart +lib/model/server_config_dto.dart lib/model/server_features_dto.dart lib/model/server_info_response_dto.dart lib/model/server_media_types_response_dto.dart @@ -265,6 +268,7 @@ lib/model/system_config_dto.dart lib/model/system_config_f_fmpeg_dto.dart lib/model/system_config_job_dto.dart lib/model/system_config_machine_learning_dto.dart +lib/model/system_config_map_dto.dart lib/model/system_config_o_auth_dto.dart lib/model/system_config_password_login_dto.dart lib/model/system_config_storage_template_dto.dart @@ -382,6 +386,7 @@ test/search_explore_response_dto_test.dart test/search_facet_count_response_dto_test.dart test/search_facet_response_dto_test.dart test/search_response_dto_test.dart +test/server_config_dto_test.dart test/server_features_dto_test.dart test/server_info_api_test.dart test/server_info_response_dto_test.dart @@ -401,6 +406,7 @@ test/system_config_dto_test.dart test/system_config_f_fmpeg_dto_test.dart test/system_config_job_dto_test.dart test/system_config_machine_learning_dto_test.dart +test/system_config_map_dto_test.dart test/system_config_o_auth_dto_test.dart test/system_config_password_login_dto_test.dart test/system_config_storage_template_dto_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52e6801752..096cc97158 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/ServerConfigDto.md b/mobile/openapi/doc/ServerConfigDto.md new file mode 100644 index 0000000000..f40c28a655 Binary files /dev/null and b/mobile/openapi/doc/ServerConfigDto.md differ diff --git a/mobile/openapi/doc/ServerFeaturesDto.md b/mobile/openapi/doc/ServerFeaturesDto.md index 168479b990..c5f7946f30 100644 Binary files a/mobile/openapi/doc/ServerFeaturesDto.md and b/mobile/openapi/doc/ServerFeaturesDto.md differ diff --git a/mobile/openapi/doc/ServerInfoApi.md b/mobile/openapi/doc/ServerInfoApi.md index 62758afbe8..c196bf1081 100644 Binary files a/mobile/openapi/doc/ServerInfoApi.md and b/mobile/openapi/doc/ServerInfoApi.md differ diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index 6fc9808528..40d528b401 100644 Binary files a/mobile/openapi/doc/SystemConfigDto.md and b/mobile/openapi/doc/SystemConfigDto.md differ diff --git a/mobile/openapi/doc/SystemConfigMapDto.md b/mobile/openapi/doc/SystemConfigMapDto.md new file mode 100644 index 0000000000..99fa91fd2f Binary files /dev/null and b/mobile/openapi/doc/SystemConfigMapDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 73a6d84e1b..cec7ba904f 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/server_info_api.dart b/mobile/openapi/lib/api/server_info_api.dart index 3b7899cc26..51bcbfa4b0 100644 Binary files a/mobile/openapi/lib/api/server_info_api.dart and b/mobile/openapi/lib/api/server_info_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 38ffa7f1bd..063e1cb27d 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart new file mode 100644 index 0000000000..75fe4b2bf7 Binary files /dev/null and b/mobile/openapi/lib/model/server_config_dto.dart differ diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 60827add6c..07a725232f 100644 Binary files a/mobile/openapi/lib/model/server_features_dto.dart and b/mobile/openapi/lib/model/server_features_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index da0500ebf8..2fe46a039a 100644 Binary files a/mobile/openapi/lib/model/system_config_dto.dart and b/mobile/openapi/lib/model/system_config_dto.dart differ diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart new file mode 100644 index 0000000000..1d39f7152e Binary files /dev/null and b/mobile/openapi/lib/model/system_config_map_dto.dart differ diff --git a/mobile/openapi/test/server_config_dto_test.dart b/mobile/openapi/test/server_config_dto_test.dart new file mode 100644 index 0000000000..03aa0ea43b Binary files /dev/null and b/mobile/openapi/test/server_config_dto_test.dart differ diff --git a/mobile/openapi/test/server_features_dto_test.dart b/mobile/openapi/test/server_features_dto_test.dart index 2cd1387ba8..6838e12563 100644 Binary files a/mobile/openapi/test/server_features_dto_test.dart and b/mobile/openapi/test/server_features_dto_test.dart differ diff --git a/mobile/openapi/test/server_info_api_test.dart b/mobile/openapi/test/server_info_api_test.dart index 8ca3e30ef1..17e5e2c5e8 100644 Binary files a/mobile/openapi/test/server_info_api_test.dart and b/mobile/openapi/test/server_info_api_test.dart differ diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 4ea0b98bde..8951d16ca8 100644 Binary files a/mobile/openapi/test/system_config_dto_test.dart and b/mobile/openapi/test/system_config_dto_test.dart differ diff --git a/mobile/openapi/test/system_config_map_dto_test.dart b/mobile/openapi/test/system_config_map_dto_test.dart new file mode 100644 index 0000000000..d0b753dc0c Binary files /dev/null and b/mobile/openapi/test/system_config_map_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 2495001daa..ef6bee00f3 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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": { diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index b9cdd181f3..105fc1bd2d 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -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; } diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index 6c8d194641..4363e379b6 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -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(); }); }); diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 655b21603e..86dc4b8d06 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -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 { + 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 { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts new file mode 100644 index 0000000000..479b1486d5 --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-map.dto.ts @@ -0,0 +1,9 @@ +import { IsBoolean, IsString } from 'class-validator'; + +export class SystemConfigMapDto { + @IsBoolean() + enabled!: boolean; + + @IsString() + tileUrl!: string; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index c089da3df3..9ec7de6225 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -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() diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index cfe64b0a79..e33a168e8d 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -55,7 +55,6 @@ export const defaults = Object.freeze({ [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({ 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', diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 7e28d041c1..24d5914b03 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -73,6 +73,10 @@ const updatedConfig = Object.freeze({ maxDistance: 0.6, }, }, + map: { + enabled: true, + tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + }, oauth: { autoLaunch: true, autoRegister: true, diff --git a/server/src/immich/controllers/server-info.controller.ts b/server/src/immich/controllers/server-info.controller.ts index 3b69c35321..b286329ab0 100644 --- a/server/src/immich/controllers/server-info.controller.ts +++ b/server/src/immich/controllers/server-info.controller.ts @@ -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 { + return this.service.getConfig(); + } + @AdminRoute() @Get('stats') getStats(): Promise { diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 2da905d10a..ebcdcac07d 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -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; diff --git a/server/test/e2e/server-info.e2e-spec.ts b/server/test/e2e/server-info.e2e-spec.ts index d04415dc69..2d344996d4 100644 --- a/server/test/e2e/server-info.e2e-spec.ts +++ b/server/test/e2e/server-info.e2e-spec.ts @@ -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'); diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 2b2a31b711..8aa65b66f1 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -2343,6 +2343,31 @@ export interface SearchResponseDto { */ 'assets': SearchAssetResponseDto; } +/** + * + * @export + * @interface ServerConfigDto + */ +export interface ServerConfigDto { + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'loginPageMessage': string; + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'mapTileUrl': string; + /** + * + * @type {string} + * @memberof ServerConfigDto + */ + 'oauthButtonText': string; +} /** * * @export @@ -2367,6 +2392,12 @@ export interface ServerFeaturesDto { * @memberof ServerFeaturesDto */ 'facialRecognition': boolean; + /** + * + * @type {boolean} + * @memberof ServerFeaturesDto + */ + 'map': boolean; /** * * @type {boolean} @@ -2810,6 +2841,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'machineLearning': SystemConfigMachineLearningDto; + /** + * + * @type {SystemConfigMapDto} + * @memberof SystemConfigDto + */ + 'map': SystemConfigMapDto; /** * * @type {SystemConfigOAuthDto} @@ -3050,6 +3087,25 @@ export interface SystemConfigMachineLearningDto { */ 'url': string; } +/** + * + * @export + * @interface SystemConfigMapDto + */ +export interface SystemConfigMapDto { + /** + * + * @type {boolean} + * @memberof SystemConfigMapDto + */ + 'enabled': boolean; + /** + * + * @type {string} + * @memberof SystemConfigMapDto + */ + 'tileUrl': string; +} /** * * @export @@ -10825,6 +10881,35 @@ export class SearchApi extends BaseAPI { */ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configuration) { return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getServerConfig: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/server-info/config`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {*} [options] Override http request option. @@ -11027,6 +11112,15 @@ export const ServerInfoApiAxiosParamCreator = function (configuration?: Configur export const ServerInfoApiFp = function(configuration?: Configuration) { const localVarAxiosParamCreator = ServerInfoApiAxiosParamCreator(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getServerConfig(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getServerConfig(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -11091,6 +11185,14 @@ export const ServerInfoApiFp = function(configuration?: Configuration) { export const ServerInfoApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { const localVarFp = ServerInfoApiFp(configuration) return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getServerConfig(options?: AxiosRequestConfig): AxiosPromise { + return localVarFp.getServerConfig(options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -11149,6 +11251,16 @@ export const ServerInfoApiFactory = function (configuration?: Configuration, bas * @extends {BaseAPI} */ export class ServerInfoApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ServerInfoApi + */ + public getServerConfig(options?: AxiosRequestConfig) { + return ServerInfoApiFp(this.configuration).getServerConfig(options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 441210fc2a..38230afd29 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -4,7 +4,7 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; import { AllJobStatusResponseDto, api, JobCommand, JobCommandDto, JobName } from '@api'; import type { ComponentType } from 'svelte'; diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte new file mode 100644 index 0000000000..dff3c65a36 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte @@ -0,0 +1,98 @@ + + +
+ {#await getConfigs() then} +
+
+
+ + +
+ + + + +
+
+
+ {/await} +
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 53bff125fb..cea013424f 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -1,18 +1,19 @@ diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 2a71f5e829..c34183bcb5 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -16,7 +16,7 @@ import IconButton from '$lib/components/elements/buttons/icon-button.svelte'; import Cog from 'svelte-material-icons/Cog.svelte'; import UserAvatar from '../user-avatar.svelte'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags } from '$lib/stores/server-config.store'; export let user: UserResponseDto; export let showUploadButton = true; diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 770bbfa238..c6ac3b13a3 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -17,7 +17,7 @@ import SideBarButton from './side-bar-button.svelte'; import { locale } from '$lib/stores/preferences.store'; import SideBarSection from './side-bar-section.svelte'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags } from '$lib/stores/server-config.store'; const getStats = async (dto: AssetApiGetAssetStatsRequest) => { const { data: stats } = await api.assetApi.getAssetStats(dto); @@ -62,9 +62,11 @@ {/if} - - - + {#if $featureFlags.map} + + + + {/if} import { goto } from '$app/navigation'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags } from '$lib/stores/server-config.store'; import { oauth, UserResponseDto } from '@api'; import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index a30539c297..0aa9a6df80 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -1,7 +1,7 @@ - -
- {#if leaflet} - {@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet} - - +
+ {#if leaflet} + {@const { Map, TileLayer, AssetMarkerCluster, Control } = leaflet} + OpenStreetMap', + maxBounds: [ + [-90, -180], + [90, 180], + ], + minZoom: 2, }} - /> - onViewAssets(detail.assetIds, detail.activeAssetIndex)} - /> - - - - - {/if} -
- + > + OpenStreetMap', + }} + /> + onViewAssets(detail.assetIds, detail.activeAssetIndex)} + /> + + + + + {/if} +
+ - - {#if $showAssetViewer} - 1} - on:next={navigateNext} - on:previous={navigatePrevious} - on:close={() => assetViewingStore.showAssetViewer(false)} + + {#if $showAssetViewer} + 1} + on:next={navigateNext} + on:previous={navigatePrevious} + on:close={() => assetViewingStore.showAssetViewer(false)} + /> + {/if} + + + {#if showSettingsModal} + (showSettingsModal = false)} + on:save={async ({ detail }) => { + const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); + showSettingsModal = false; + $mapSettings = detail; + + if (shouldUpdate) { + mapMarkers = await loadMapMarkers(); + } + }} /> {/if} - - -{#if showSettingsModal} - (showSettingsModal = false)} - on:save={async ({ detail }) => { - const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); - showSettingsModal = false; - $mapSettings = detail; - - if (shouldUpdate) { - mapMarkers = await loadMapMarkers(); - } - }} - /> {/if} diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 519cdd7868..9ed7bf2828 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -14,7 +14,7 @@ import AppleHeader from '$lib/components/shared-components/apple-header.svelte'; import FaviconHeader from '$lib/components/shared-components/favicon-header.svelte'; import { onMount } from 'svelte'; - import { loadFeatureFlags } from '$lib/stores/feature-flags.store'; + import { loadConfig } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { api } from '@api'; @@ -37,9 +37,9 @@ onMount(async () => { try { - await loadFeatureFlags(); + await loadConfig(); } catch (error) { - handleError(error, 'Unable to load feature flags'); + handleError(error, 'Unable to connect to server'); } }); diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index e7626148eb..fe8a78d552 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -3,6 +3,7 @@ import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte'; + import MapSettings from '$lib/components/admin-page/settings/map-settings/map-settings.svelte'; import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte'; import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte'; import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte'; @@ -11,7 +12,7 @@ import Button from '$lib/components/elements/buttons/button.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { downloadManager } from '$lib/stores/download'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags } from '$lib/stores/server-config.store'; import { downloadBlob } from '$lib/utils/asset-utils'; import { SystemConfigDto, api, copyToClipboard } from '@api'; import Alert from 'svelte-material-icons/Alert.svelte'; @@ -57,20 +58,6 @@ Export as JSON - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + {/await} diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index c3e4e8a418..4b11481f64 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -3,18 +3,17 @@ import LoginForm from '$lib/components/forms/login-form.svelte'; import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte'; import { AppRoute } from '$lib/constants'; - import { loginPageMessage } from '$lib/constants'; - import { featureFlags } from '$lib/stores/feature-flags.store'; + import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import type { PageData } from './$types'; export let data: PageData; {#if $featureFlags.loaded} - +

- {@html loginPageMessage} + {@html $serverConfig.loginPageMessage}