From a7719a94fcac4e52edefe482a7661019708fde53 Mon Sep 17 00:00:00 2001
From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Date: Mon, 23 Sep 2024 17:40:25 +0200
Subject: [PATCH] fix: normalize external domain (#12831)

chore: normalize external domain
---
 server/src/cores/system-config.core.ts          |  4 ++++
 .../src/services/system-config.service.spec.ts  | 17 +++++++++++++++++
 web/src/lib/utils.ts                            |  6 +-----
 3 files changed, 22 insertions(+), 5 deletions(-)

diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts
index 7c1434004a..8ed53344cc 100644
--- a/server/src/cores/system-config.core.ts
+++ b/server/src/cores/system-config.core.ts
@@ -120,6 +120,10 @@ export class SystemConfigCore {
       }
     }
 
+    if (config.server.externalDomain.length > 0) {
+      config.server.externalDomain = new URL(config.server.externalDomain).origin;
+    }
+
     if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
       config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
     }
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 409cd6a52f..7e25e0cd46 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -289,6 +289,23 @@ describe(SystemConfigService.name, () => {
       expect(config.machineLearning.url).toEqual('immich_machine_learning');
     });
 
+    const externalDomainTests = [
+      { should: 'with a trailing slash', externalDomain: 'https://demo.immich.app/' },
+      { should: 'without a trailing slash', externalDomain: 'https://demo.immich.app' },
+      { should: 'with a port', externalDomain: 'https://demo.immich.app:42', result: 'https://demo.immich.app:42' },
+    ];
+
+    for (const { should, externalDomain, result } of externalDomainTests) {
+      it(`should normalize an external domain ${should}`, async () => {
+        process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
+        const partialConfig = { server: { externalDomain } };
+        systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig));
+
+        const config = await sut.getConfig();
+        expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app');
+      });
+    }
+
     it('should warn for unknown options in yaml', async () => {
       process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml';
       const partialConfig = `
diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts
index 29c7552d0c..dccb03c9bf 100644
--- a/web/src/lib/utils.ts
+++ b/web/src/lib/utils.ts
@@ -257,11 +257,7 @@ export const copyToClipboard = async (secret: string) => {
 };
 
 export const makeSharedLinkUrl = (externalDomain: string, key: string) => {
-  let url = externalDomain || window.location.origin;
-  if (!url.endsWith('/')) {
-    url += '/';
-  }
-  return `${url}share/${key}`;
+  return new URL(`share/${key}`, externalDomain || window.location.origin).href;
 };
 
 export const oauth = {