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

feat(web,server): external domain setting (#6146)

* feat: external domain setting

* chore: open api

* mobile: handle serverconfig-externalDomain

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
Jason Rasmussen 2024-01-03 21:54:48 -05:00 committed by GitHub
parent 1e503c3212
commit 317adc5c28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 259 additions and 21 deletions

View File

@ -3007,6 +3007,12 @@ export interface SearchResponseDto {
* @interface ServerConfigDto
*/
export interface ServerConfigDto {
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'externalDomain': string;
/**
*
* @type {boolean}
@ -3590,6 +3596,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'reverseGeocoding': SystemConfigReverseGeocodingDto;
/**
*
* @type {SystemConfigServerDto}
* @memberof SystemConfigDto
*/
'server': SystemConfigServerDto;
/**
*
* @type {SystemConfigStorageTemplateDto}
@ -4014,6 +4026,19 @@ export interface SystemConfigReverseGeocodingDto {
*/
'enabled': boolean;
}
/**
*
* @export
* @interface SystemConfigServerDto
*/
export interface SystemConfigServerDto {
/**
*
* @type {string}
* @memberof SystemConfigServerDto
*/
'externalDomain': string;
}
/**
*
* @export

View File

@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
@ -71,7 +72,11 @@ class SharedLinkItem extends ConsumerWidget {
final imageSize = math.min(context.width / 4, 100.0);
void copyShareLinkToClipboard() {
final serverUrl = getServerUrl();
final externalDomain = ref.read(
serverInfoProvider.select((s) => s.serverConfig.externalDomain),
);
final serverUrl =
externalDomain.isNotEmpty ? externalDomain : getServerUrl();
if (serverUrl == null) {
ImmichToast.show(
context: context,

View File

@ -8,6 +8,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/modules/shared_link/models/shared_link.dart';
import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart';
import 'package:immich_mobile/modules/shared_link/services/shared_link.service.dart';
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
import 'package:immich_mobile/shared/ui/immich_toast.dart';
import 'package:immich_mobile/utils/url_helper.dart';
@ -353,7 +354,11 @@ class SharedLinkEditPage extends HookConsumerWidget {
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
);
ref.invalidate(sharedLinksStateProvider);
final serverUrl = getServerUrl();
final externalDomain = ref.read(
serverInfoProvider.select((s) => s.serverConfig.externalDomain),
);
final serverUrl =
externalDomain.isNotEmpty ? externalDomain : getServerUrl();
if (newLink != null && serverUrl != null) {
newShareLink.value = "$serverUrl/share/${newLink.key}";
copyLinkToClipboard();

View File

@ -2,35 +2,40 @@ import 'package:openapi/api.dart';
class ServerConfig {
final int trashDays;
final String externalDomain;
const ServerConfig({
required this.trashDays,
required this.externalDomain,
});
ServerConfig copyWith({
int? trashDays,
String? externalDomain,
}) {
return ServerConfig(
trashDays: trashDays ?? this.trashDays,
externalDomain: externalDomain ?? this.externalDomain,
);
}
@override
String toString() {
return 'ServerConfig(trashDays: $trashDays)';
}
String toString() =>
'ServerConfig(trashDays: $trashDays, externalDomain: $externalDomain)';
ServerConfig.fromDto(ServerConfigDto dto) : trashDays = dto.trashDays;
ServerConfig.fromDto(ServerConfigDto dto)
: trashDays = dto.trashDays,
externalDomain = dto.externalDomain;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ServerConfig && other.trashDays == trashDays;
return other is ServerConfig &&
other.trashDays == trashDays &&
other.externalDomain == externalDomain;
}
@override
int get hashCode {
return trashDays.hashCode;
}
int get hashCode => trashDays.hashCode ^ externalDomain.hashCode;
}

View File

@ -29,6 +29,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
),
serverConfig: const ServerConfig(
trashDays: 30,
externalDomain: '',
),
serverDiskInfo: const ServerDiskInfo(
diskAvailable: "0",
@ -74,7 +75,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
if (appVersion["major"]! > serverVersion.major) {
state = state.copyWith(
isVersionMismatch: true,
versionMismatchErrorMessage: "profile_drawer_server_out_of_date_major".tr(),
versionMismatchErrorMessage:
"profile_drawer_server_out_of_date_major".tr(),
);
return;
}
@ -82,7 +84,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
if (appVersion["major"]! < serverVersion.major) {
state = state.copyWith(
isVersionMismatch: true,
versionMismatchErrorMessage: "profile_drawer_client_out_of_date_major".tr(),
versionMismatchErrorMessage:
"profile_drawer_client_out_of_date_major".tr(),
);
return;
}
@ -90,7 +93,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
if (appVersion["minor"]! > serverVersion.minor) {
state = state.copyWith(
isVersionMismatch: true,
versionMismatchErrorMessage: "profile_drawer_server_out_of_date_minor".tr(),
versionMismatchErrorMessage:
"profile_drawer_server_out_of_date_minor".tr(),
);
return;
}
@ -98,7 +102,8 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
if (appVersion["minor"]! < serverVersion.minor) {
state = state.copyWith(
isVersionMismatch: true,
versionMismatchErrorMessage: "profile_drawer_client_out_of_date_minor".tr(),
versionMismatchErrorMessage:
"profile_drawer_client_out_of_date_minor".tr(),
);
return;
}

View File

@ -149,6 +149,7 @@ doc/SystemConfigNewVersionCheckDto.md
doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.md
doc/SystemConfigReverseGeocodingDto.md
doc/SystemConfigServerDto.md
doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThemeDto.md
@ -335,6 +336,7 @@ lib/model/system_config_new_version_check_dto.dart
lib/model/system_config_o_auth_dto.dart
lib/model/system_config_password_login_dto.dart
lib/model/system_config_reverse_geocoding_dto.dart
lib/model/system_config_server_dto.dart
lib/model/system_config_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_theme_dto.dart
@ -508,6 +510,7 @@ test/system_config_new_version_check_dto_test.dart
test/system_config_o_auth_dto_test.dart
test/system_config_password_login_dto_test.dart
test/system_config_reverse_geocoding_dto_test.dart
test/system_config_server_dto_test.dart
test/system_config_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart
test/system_config_theme_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -8593,6 +8593,9 @@
},
"ServerConfigDto": {
"properties": {
"externalDomain": {
"type": "string"
},
"isInitialized": {
"type": "boolean"
},
@ -8610,7 +8613,8 @@
"trashDays",
"oauthButtonText",
"loginPageMessage",
"isInitialized"
"isInitialized",
"externalDomain"
],
"type": "object"
},
@ -9039,6 +9043,9 @@
"reverseGeocoding": {
"$ref": "#/components/schemas/SystemConfigReverseGeocodingDto"
},
"server": {
"$ref": "#/components/schemas/SystemConfigServerDto"
},
"storageTemplate": {
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
},
@ -9066,7 +9073,8 @@
"thumbnail",
"trash",
"theme",
"library"
"library",
"server"
],
"type": "object"
},
@ -9359,6 +9367,17 @@
],
"type": "object"
},
"SystemConfigServerDto": {
"properties": {
"externalDomain": {
"type": "string"
}
},
"required": [
"externalDomain"
],
"type": "object"
},
"SystemConfigStorageTemplateDto": {
"properties": {
"enabled": {

View File

@ -86,6 +86,7 @@ export class ServerConfigDto {
@ApiProperty({ type: 'integer' })
trashDays!: number;
isInitialized!: boolean;
externalDomain!: string;
}
export class ServerFeaturesDto implements FeatureFlags {

View File

@ -184,6 +184,7 @@ describe(ServerInfoService.name, () => {
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
trashDays: 30,
externalDomain: '',
});
expect(configMock.load).toHaveBeenCalled();
});

View File

@ -89,6 +89,7 @@ export class ServerInfoService {
trashDays: config.trash.days,
oauthButtonText: config.oauth.buttonText,
isInitialized,
externalDomain: config.server.externalDomain,
};
}

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class SystemConfigServerDto {
@IsString()
externalDomain!: string;
}

View File

@ -11,6 +11,7 @@ import { SystemConfigNewVersionCheckDto } from './system-config-new-version-chec
import { SystemConfigOAuthDto } from './system-config-oauth.dto';
import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto';
import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto';
import { SystemConfigServerDto } from './system-config-server.dto';
import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto';
import { SystemConfigThemeDto } from './system-config-theme.dto';
import { SystemConfigThumbnailDto } from './system-config-thumbnail.dto';
@ -86,6 +87,11 @@ export class SystemConfigDto implements SystemConfig {
@ValidateNested()
@IsObject()
library!: SystemConfigLibraryDto;
@Type(() => SystemConfigServerDto)
@ValidateNested()
@IsObject()
server!: SystemConfigServerDto;
}
export function mapConfig(config: SystemConfig): SystemConfigDto {

View File

@ -127,6 +127,9 @@ export const defaults = Object.freeze<SystemConfig>({
cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT,
},
},
server: {
externalDomain: '',
},
});
export enum FeatureFlag {

View File

@ -100,6 +100,9 @@ const updatedConfig = Object.freeze<SystemConfig>({
passwordLogin: {
enabled: true,
},
server: {
externalDomain: '',
},
storageTemplate: {
enabled: false,
hashVerificationEnabled: true,

View File

@ -84,6 +84,8 @@ export enum SystemConfigKey {
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
SERVER_EXTERNAL_DOMAIN = 'server.externalDomain',
STORAGE_TEMPLATE_ENABLED = 'storageTemplate.enabled',
STORAGE_TEMPLATE_HASH_VERIFICATION_ENABLED = 'storageTemplate.hashVerificationEnabled',
STORAGE_TEMPLATE = 'storageTemplate.template',
@ -244,4 +246,7 @@ export interface SystemConfig {
cronExpression: string;
};
};
server: {
externalDomain: string;
};
}

View File

@ -97,6 +97,7 @@ describe(`${ServerInfoController.name} (e2e)`, () => {
oauthButtonText: 'Login with OAuth',
trashDays: 30,
isInitialized: true,
externalDomain: '',
});
});
});

View File

@ -3007,6 +3007,12 @@ export interface SearchResponseDto {
* @interface ServerConfigDto
*/
export interface ServerConfigDto {
/**
*
* @type {string}
* @memberof ServerConfigDto
*/
'externalDomain': string;
/**
*
* @type {boolean}
@ -3590,6 +3596,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'reverseGeocoding': SystemConfigReverseGeocodingDto;
/**
*
* @type {SystemConfigServerDto}
* @memberof SystemConfigDto
*/
'server': SystemConfigServerDto;
/**
*
* @type {SystemConfigStorageTemplateDto}
@ -4014,6 +4026,19 @@ export interface SystemConfigReverseGeocodingDto {
*/
'enabled': boolean;
}
/**
*
* @export
* @interface SystemConfigServerDto
*/
export interface SystemConfigServerDto {
/**
*
* @type {string}
* @memberof SystemConfigServerDto
*/
'externalDomain': string;
}
/**
*
* @export

View File

@ -19,6 +19,10 @@ export const copyToClipboard = async (secret: string) => {
}
};
export const makeSharedLinkUrl = (externalDomain: string, key: string) => {
return `${externalDomain || window.location.origin}/share/${key}`;
};
export const oauth = {
isCallback: (location: Location) => {
const search = location.search;

View File

@ -0,0 +1,107 @@
<script lang="ts">
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import type { ResetOptions } from '$lib/utils/dipatch';
import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigServerDto } from '@api';
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
export let serverConfig: SystemConfigServerDto; // this is the config that is being edited
export let disabled = false;
let savedConfig: SystemConfigServerDto;
let defaultConfig: SystemConfigServerDto;
const handleReset = (detail: ResetOptions) => {
if (detail.default) {
resetToDefault();
} else {
reset();
}
};
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.server),
api.systemConfigApi.getConfigDefaults().then((res) => res.data.server),
]);
}
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
serverConfig = { ...resetConfig.server };
savedConfig = { ...resetConfig.server };
notificationController.show({
message: 'Reset server settings to the recent saved settings',
type: NotificationType.Info,
});
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
serverConfig = { ...configs.server };
defaultConfig = { ...configs.server };
notificationController.show({
message: 'Reset server settings to default',
type: NotificationType.Info,
});
}
async function saveSetting() {
try {
const { data: configs } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
systemConfigDto: {
...configs,
server: serverConfig,
},
});
serverConfig = { ...result.data.server };
savedConfig = { ...result.data.server };
notificationController.show({
message: 'Server settings saved',
type: NotificationType.Info,
});
} catch (e) {
handleError(e, 'Unable to save settings');
}
}
</script>
<div>
{#await getConfigs() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="mt-2">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="EXTERNAL DOMAIN"
desc="Domain for public shared links, including http(s)://"
bind:value={serverConfig.externalDomain}
isEdited={serverConfig.externalDomain !== savedConfig.externalDomain}
/>
<div class="ml-4">
<SettingButtonsRow
on:reset={({ detail }) => handleReset(detail)}
on:save={saveSetting}
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
{disabled}
/>
</div>
</div>
</form>
</div>
{/await}
</div>

View File

@ -5,7 +5,7 @@
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import { handleError } from '$lib/utils/handle-error';
import { api, copyToClipboard, SharedLinkResponseDto, SharedLinkType } from '@api';
import { api, copyToClipboard, makeSharedLinkUrl, SharedLinkResponseDto, SharedLinkType } from '@api';
import { createEventDispatcher, onMount } from 'svelte';
import Icon from '$lib/components/elements/icon.svelte';
import BaseModal from '../base-modal.svelte';
@ -13,6 +13,7 @@
import DropdownButton from '../dropdown-button.svelte';
import { notificationController, NotificationType } from '../notification/notification';
import { mdiLink } from '@mdi/js';
import { serverConfig } from '$lib/stores/server-config.store';
export let albumId: string | undefined = undefined;
export let assetIds: string[] = [];
@ -82,7 +83,7 @@
showMetadata,
},
});
sharedLink = `${window.location.origin}/share/${data.key}`;
sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
} catch (e) {
handleError(e, 'Failed to create shared link');
}
@ -182,7 +183,7 @@
{:else}
<div class="text-sm">
Individual shared | <span class="text-immich-primary dark:text-immich-dark-primary"
>{editingLink.description}</span
>{editingLink.description || ''}</span
>
</div>
{/if}

View File

@ -26,6 +26,7 @@ export const serverConfig = writable<ServerConfig>({
loginPageMessage: '',
trashDays: 30,
isInitialized: false,
externalDomain: '',
});
export const loadConfig = async () => {

View File

@ -1,6 +1,6 @@
<script lang="ts">
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { api, copyToClipboard, SharedLinkResponseDto } from '@api';
import { api, copyToClipboard, makeSharedLinkUrl, SharedLinkResponseDto } from '@api';
import { goto } from '$app/navigation';
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
import {
@ -13,6 +13,7 @@
import { handleError } from '$lib/utils/handle-error';
import { AppRoute } from '$lib/constants';
import { mdiArrowLeft } from '@mdi/js';
import { serverConfig } from '$lib/stores/server-config.store';
let sharedLinks: SharedLinkResponseDto[] = [];
let editSharedLink: SharedLinkResponseDto | null = null;
@ -49,7 +50,7 @@
};
const handleCopyLink = async (key: string) => {
await copyToClipboard(`${window.location.origin}/share/${key}`);
await copyToClipboard(makeSharedLinkUrl($serverConfig.externalDomain, key));
};
</script>

View File

@ -9,6 +9,7 @@
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
import ServerSettings from '$lib/components/admin-page/settings/server/server-settings.svelte';
import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte';
import ThemeSettings from '$lib/components/admin-page/settings/theme/theme-settings.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@ -95,6 +96,10 @@
<PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
</SettingAccordion>
<SettingAccordion title="Server Settings" subtitle="Manage server settings">
<ServerSettings disabled={$featureFlags.configFile} serverConfig={configs.server} />
</SettingAccordion>
<SettingAccordion
title="Storage Template"
subtitle="Manage the folder structure and file name of the upload asset"