mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
Merge 76eb0db9fa into 675408b003
This commit is contained in:
commit
f01e7c4f08
25 changed files with 106 additions and 14 deletions
|
|
@ -168,7 +168,11 @@ Override the default notifications text with notification templates. More inform
|
|||
|
||||
### External Domain
|
||||
|
||||
Overrides the domain name in shared links and email notifications. The URL should not include a trailing slash.
|
||||
Overrides the domain name in email notifications. The URL should not include a trailing slash.
|
||||
|
||||
### Shared Link Domain
|
||||
|
||||
Overrides the domain name in shared links. The URL should not include a trailing slash.
|
||||
|
||||
### Welcome Message
|
||||
|
||||
|
|
|
|||
|
|
@ -217,6 +217,7 @@ The default configuration looks like this:
|
|||
},
|
||||
"server": {
|
||||
"externalDomain": "",
|
||||
"sharedLinkDomain": "",
|
||||
"loginPageMessage": "",
|
||||
"publicUsers": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -138,6 +138,7 @@ describe('/server', () => {
|
|||
userDeleteDelay: 7,
|
||||
isInitialized: true,
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
publicUsers: true,
|
||||
isOnboarded: false,
|
||||
maintenanceMode: false,
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||
isInitialized: true,
|
||||
isOnboarded: true,
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
publicUsers: true,
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@
|
|||
"server_public_users_description": "Alle brugere (navn og e-mail) vises, når en bruger tilføjes til delte album. Når den er deaktiveret, vil brugerlisten kun være tilgængelig for administratorbrugere.",
|
||||
"server_settings": "Serverindstillinger",
|
||||
"server_settings_description": "Administrér serverindstillinger",
|
||||
"server_shared_link_domain_settings": "Delt link domæne",
|
||||
"server_shared_link_domain_settings_description": "Domæne brugt til delte links",
|
||||
"server_stats_page_description": "Admin server statistikside",
|
||||
"server_welcome_message": "Velkomstbesked",
|
||||
"server_welcome_message_description": "En besked som bliver vist på loginsiden.",
|
||||
|
|
|
|||
|
|
@ -347,6 +347,8 @@
|
|||
"server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.",
|
||||
"server_settings": "Server Settings",
|
||||
"server_settings_description": "Manage server settings",
|
||||
"server_shared_link_domain_settings": "Shared link domain",
|
||||
"server_shared_link_domain_settings_description": "Domain used for shared links",
|
||||
"server_stats_page_description": "Admin server statistics page",
|
||||
"server_welcome_message": "Welcome message",
|
||||
"server_welcome_message_description": "A message that is displayed on the login page.",
|
||||
|
|
|
|||
|
|
@ -346,6 +346,8 @@
|
|||
"server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.",
|
||||
"server_settings": "Server Settings",
|
||||
"server_settings_description": "Manage server settings",
|
||||
"server_shared_link_domain_settings": "Shared link domain",
|
||||
"server_shared_link_domain_settings_description": "Domain used for shared links",
|
||||
"server_stats_page_description": "Admin server statistics page",
|
||||
"server_welcome_message": "Welcome message",
|
||||
"server_welcome_message_description": "A message that is displayed on the login page.",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class ServerConfig {
|
|||
final int trashDays;
|
||||
final String oauthButtonText;
|
||||
final String externalDomain;
|
||||
final String sharedLinkDomain;
|
||||
final String mapDarkStyleUrl;
|
||||
final String mapLightStyleUrl;
|
||||
|
||||
|
|
@ -11,15 +12,17 @@ class ServerConfig {
|
|||
required this.trashDays,
|
||||
required this.oauthButtonText,
|
||||
required this.externalDomain,
|
||||
required this.sharedLinkDomain,
|
||||
required this.mapDarkStyleUrl,
|
||||
required this.mapLightStyleUrl,
|
||||
});
|
||||
|
||||
ServerConfig copyWith({int? trashDays, String? oauthButtonText, String? externalDomain}) {
|
||||
ServerConfig copyWith({int? trashDays, String? oauthButtonText, String? externalDomain, String? sharedLinkDomain}) {
|
||||
return ServerConfig(
|
||||
trashDays: trashDays ?? this.trashDays,
|
||||
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
||||
externalDomain: externalDomain ?? this.externalDomain,
|
||||
sharedLinkDomain: sharedLinkDomain ?? this.sharedLinkDomain,
|
||||
mapDarkStyleUrl: mapDarkStyleUrl,
|
||||
mapLightStyleUrl: mapLightStyleUrl,
|
||||
);
|
||||
|
|
@ -27,12 +30,13 @@ class ServerConfig {
|
|||
|
||||
@override
|
||||
String toString() =>
|
||||
'ServerConfig(trashDays: $trashDays, oauthButtonText: $oauthButtonText, externalDomain: $externalDomain)';
|
||||
'ServerConfig(trashDays: $trashDays, oauthButtonText: $oauthButtonText, externalDomain: $externalDomain, sharedLinkDomain: $sharedLinkDomain)';
|
||||
|
||||
ServerConfig.fromDto(ServerConfigDto dto)
|
||||
: trashDays = dto.trashDays,
|
||||
oauthButtonText = dto.oauthButtonText,
|
||||
externalDomain = dto.externalDomain,
|
||||
sharedLinkDomain = dto.sharedLinkDomain,
|
||||
mapDarkStyleUrl = dto.mapDarkStyleUrl,
|
||||
mapLightStyleUrl = dto.mapLightStyleUrl;
|
||||
|
||||
|
|
@ -44,9 +48,11 @@ class ServerConfig {
|
|||
|
||||
return other.trashDays == trashDays &&
|
||||
other.oauthButtonText == oauthButtonText &&
|
||||
other.externalDomain == externalDomain;
|
||||
other.externalDomain == externalDomain &&
|
||||
other.sharedLinkDomain == sharedLinkDomain;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => trashDays.hashCode ^ oauthButtonText.hashCode ^ externalDomain.hashCode;
|
||||
int get hashCode =>
|
||||
trashDays.hashCode ^ oauthButtonText.hashCode ^ externalDomain.hashCode ^ sharedLinkDomain.hashCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeData = context.themeData;
|
||||
final colorScheme = context.colorScheme;
|
||||
final externalDomain = ref.watch(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||
final displayServerUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||
final sharedLinkDomain = ref.watch(serverInfoProvider.select((s) => s.serverConfig.sharedLinkDomain));
|
||||
final displayServerUrl = sharedLinkDomain.isNotEmpty ? sharedLinkDomain : getServerUrl();
|
||||
final expiryPresets = <(Duration, String)>[
|
||||
(Duration.zero, context.t.never),
|
||||
(const Duration(minutes: 30), context.t.shared_link_edit_expire_after_option_minutes(count: 30)),
|
||||
|
|
@ -345,9 +345,9 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||
final sharedLinkDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.sharedLinkDomain));
|
||||
|
||||
final serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||
final serverUrl = sharedLinkDomain.isNotEmpty ? sharedLinkDomain : getServerUrl();
|
||||
|
||||
if (newLink != null) {
|
||||
newShareLink.value = buildSharedLinkUrl(baseUrl: serverUrl, slug: newLink.slug, key: newLink.key) ?? '';
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
|
|||
trashDays: 30,
|
||||
oauthButtonText: '',
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
),
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ final Map<String, Map<String, Object?>> openApiPatches = {
|
|||
'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
'mapDarkStyleUrl': 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
'minFaces': 3,
|
||||
'sharedLinkDomain': '',
|
||||
},
|
||||
'SystemConfigServerDto': {'sharedLinkDomain': ''},
|
||||
'UserResponseDto': {'profileChangedAt': _now},
|
||||
'AssetResponseDto': {'visibility': 'timeline', 'createdAt': _now, 'isEdited': false},
|
||||
'UserAdminResponseDto': {'profileChangedAt': _now},
|
||||
|
|
|
|||
|
|
@ -22526,6 +22526,10 @@
|
|||
"description": "Whether public user registration is enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sharedLinkDomain": {
|
||||
"description": "Shared link domain URL",
|
||||
"type": "string"
|
||||
},
|
||||
"trashDays": {
|
||||
"description": "Number of days before trashed assets are permanently deleted",
|
||||
"maximum": 9007199254740991,
|
||||
|
|
@ -22550,6 +22554,7 @@
|
|||
"minFaces",
|
||||
"oauthButtonText",
|
||||
"publicUsers",
|
||||
"sharedLinkDomain",
|
||||
"trashDays",
|
||||
"userDeleteDelay"
|
||||
],
|
||||
|
|
@ -26435,12 +26440,17 @@
|
|||
"publicUsers": {
|
||||
"description": "Public users",
|
||||
"type": "boolean"
|
||||
},
|
||||
"sharedLinkDomain": {
|
||||
"description": "Shared link domain",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"externalDomain",
|
||||
"loginPageMessage",
|
||||
"publicUsers"
|
||||
"publicUsers",
|
||||
"sharedLinkDomain"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2013,6 +2013,8 @@ export type ServerConfigDto = {
|
|||
oauthButtonText: string;
|
||||
/** Whether public user registration is enabled */
|
||||
publicUsers: boolean;
|
||||
/** Shared link domain URL */
|
||||
sharedLinkDomain: string;
|
||||
/** Number of days before trashed assets are permanently deleted */
|
||||
trashDays: number;
|
||||
/** Delay in days before deleted users are permanently removed */
|
||||
|
|
@ -2581,6 +2583,8 @@ export type SystemConfigServerDto = {
|
|||
loginPageMessage: string;
|
||||
/** Public users */
|
||||
publicUsers: boolean;
|
||||
/** Shared link domain */
|
||||
sharedLinkDomain: string;
|
||||
};
|
||||
export type SystemConfigStorageTemplateDto = {
|
||||
/** Enabled */
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ export type SystemConfig = {
|
|||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
sharedLinkDomain: string;
|
||||
loginPageMessage: string;
|
||||
publicUsers: boolean;
|
||||
};
|
||||
|
|
@ -418,6 +419,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
loginPageMessage: '',
|
||||
publicUsers: true,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ const ServerConfigSchema = z
|
|||
isInitialized: z.boolean().describe('Whether the server has been initialized'),
|
||||
isOnboarded: z.boolean().describe('Whether the admin has completed onboarding'),
|
||||
externalDomain: z.string().describe('External domain URL'),
|
||||
sharedLinkDomain: z.string().describe('Shared link domain URL'),
|
||||
publicUsers: z.boolean().describe('Whether public user registration is enabled'),
|
||||
mapDarkStyleUrl: z.string().describe('Map dark style URL'),
|
||||
mapLightStyleUrl: z.string().describe('Map light style URL'),
|
||||
|
|
|
|||
|
|
@ -300,6 +300,12 @@ const SystemConfigServerSchema = z
|
|||
error: 'External domain must be an empty string or a valid URL',
|
||||
})
|
||||
.describe('External domain'),
|
||||
sharedLinkDomain: z
|
||||
.string()
|
||||
.refine((url) => url.length === 0 || z.url().safeParse(url).success, {
|
||||
error: 'Shared link domain must be an empty string or a valid URL',
|
||||
})
|
||||
.describe('Shared link domain'),
|
||||
loginPageMessage: z.string().describe('Login page message'),
|
||||
publicUsers: configBool.describe('Public users'),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -165,6 +165,7 @@ describe(ServerService.name, () => {
|
|||
isInitialized: false,
|
||||
isOnboarded: false,
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
publicUsers: true,
|
||||
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
|
||||
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export class ServerService extends BaseService {
|
|||
isInitialized,
|
||||
isOnboarded: onboarding?.isOnboarded || false,
|
||||
externalDomain: config.server.externalDomain,
|
||||
sharedLinkDomain: config.server.sharedLinkDomain,
|
||||
publicUsers: config.server.publicUsers,
|
||||
mapDarkStyleUrl: config.map.darkStyle,
|
||||
mapLightStyleUrl: config.map.lightStyle,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from 'src/dtos/shared-link.dto';
|
||||
import { Permission, SharedLinkType } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { getExternalDomain, OpenGraphTags } from 'src/utils/misc';
|
||||
import { getSharedLinkDomain, OpenGraphTags } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class SharedLinkService extends BaseService {
|
||||
|
|
@ -232,7 +232,7 @@ export class SharedLinkService extends BaseService {
|
|||
return {
|
||||
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
|
||||
description: sharedLink.description || `${assetCount} shared photos & videos`,
|
||||
imageUrl: new URL(imagePath, getExternalDomain(config.server, defaultDomain)).href,
|
||||
imageUrl: new URL(imagePath, getSharedLinkDomain(config.server, defaultDomain)).href,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
sharedLinkDomain: '',
|
||||
loginPageMessage: '',
|
||||
publicUsers: true,
|
||||
},
|
||||
|
|
@ -425,6 +426,28 @@ describe(SystemConfigService.name, () => {
|
|||
});
|
||||
}
|
||||
|
||||
const sharedLinkDomainTests = [
|
||||
{ should: 'with a trailing slash', sharedLinkDomain: 'https://demo.immich.app/' },
|
||||
{ should: 'without a trailing slash', sharedLinkDomain: 'https://demo.immich.app' },
|
||||
{ should: 'with a port', sharedLinkDomain: 'https://demo.immich.app:42', result: 'https://demo.immich.app:42' },
|
||||
{
|
||||
should: 'with basic auth',
|
||||
sharedLinkDomain: 'https://user:password@example.com:123',
|
||||
result: 'https://user:password@example.com:123',
|
||||
},
|
||||
];
|
||||
|
||||
for (const { should, sharedLinkDomain, result } of sharedLinkDomainTests) {
|
||||
it(`should normalize an external domain ${should}`, async () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
const partialConfig = { server: { sharedLinkDomain } };
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(partialConfig));
|
||||
|
||||
const config = await sut.getSystemConfig();
|
||||
expect(config.server.sharedLinkDomain).toEqual(result ?? 'https://demo.immich.app');
|
||||
});
|
||||
}
|
||||
|
||||
it('should warn for unknown options in yaml', async () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.yaml' }));
|
||||
const partialConfig = `
|
||||
|
|
|
|||
|
|
@ -126,6 +126,17 @@ const buildConfig = async (repos: RepoDeps) => {
|
|||
config.server.externalDomain = externalDomain;
|
||||
}
|
||||
|
||||
if (config.server.sharedLinkDomain.length > 0) {
|
||||
const domain = new URL(config.server.sharedLinkDomain);
|
||||
|
||||
const sharedLinkDomain =
|
||||
domain.password && domain.username
|
||||
? `${domain.protocol}//${domain.username}:${domain.password}@${domain.host}`
|
||||
: domain.origin;
|
||||
|
||||
config.server.sharedLinkDomain = sharedLinkDomain;
|
||||
}
|
||||
|
||||
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
|
||||
config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ export const getMethodNames = (instance: any) => {
|
|||
export const getExternalDomain = (server: SystemConfig['server'], defaultDomain = 'https://my.immich.app') =>
|
||||
server.externalDomain || defaultDomain;
|
||||
|
||||
export const getSharedLinkDomain = (server: SystemConfig['server'], defaultDomain = 'https://my.immich.app') =>
|
||||
server.sharedLinkDomain || defaultDomain;
|
||||
|
||||
/**
|
||||
* @returns a list of strings representing the keys of the object in dot notation
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
|||
|
||||
vi.mock(import('$lib/managers/server-config-manager.svelte'), () => ({
|
||||
serverConfigManager: {
|
||||
value: { externalDomain: 'http://localhost:2283' } as ServerConfigDto,
|
||||
value: { sharedLinkDomain: 'http://localhost:2283' } as ServerConfigDto,
|
||||
init: vi.fn(),
|
||||
loadServerConfig: vi.fn(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
|
|||
|
||||
export const asUrl = (sharedLink: SharedLinkResponseDto) => {
|
||||
const path = Route.viewSharedLink(sharedLink);
|
||||
return new URL(path, serverConfigManager.value.externalDomain || location.origin).href;
|
||||
return new URL(path, serverConfigManager.value.sharedLinkDomain || location.origin).href;
|
||||
};
|
||||
|
||||
export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,14 @@
|
|||
isEdited={configToEdit.server.externalDomain !== config.server.externalDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_shared_link_domain_settings')}
|
||||
description={$t('admin.server_shared_link_domain_settings_description')}
|
||||
bind:value={configToEdit.server.sharedLinkDomain}
|
||||
isEdited={configToEdit.server.sharedLinkDomain !== config.server.sharedLinkDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_welcome_message')}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue