diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 5e3e0983..09606e25 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -41,8 +41,11 @@ jobs: run: | set -euo pipefail SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")" + TINODE_API_KEY="${TINODE_API_KEY#\'}" + TINODE_API_KEY="${TINODE_API_KEY%\'}" docker build \ --build-arg TINODE_API_KEY="${TINODE_API_KEY}" \ + --build-arg TINODE_API_BASE="https://im-api.yuexuan.shop/v1/im" \ --build-arg TINODE_FILE_BASE="https://im-api.yuexuan.shop/v0/file" \ -t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" . docker push "$IMAGE:pro" diff --git a/Dockerfile b/Dockerfile index 683f0ae4..f47a371e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,11 @@ COPY web/. . ARG TINODE_API_KEY= ENV VITE_TINODE_API_KEY=$TINODE_API_KEY +# The production web UI and IM REST API use different origins. Local development leaves this unset +# and continues to use Vite's same-origin /v1/im proxy. +ARG TINODE_API_BASE= +ENV VITE_TINODE_API_BASE=$TINODE_API_BASE + # File downloads must use the canonical IM API origin. The web origin has its own independently # deployed path router and may otherwise send /v0/file requests to an older IM server. ARG TINODE_FILE_BASE=https://api.118im.com/v0/file diff --git a/tests/test_actions_workflow.py b/tests/test_actions_workflow.py index d38444b3..f0e9c829 100644 --- a/tests/test_actions_workflow.py +++ b/tests/test_actions_workflow.py @@ -3,12 +3,16 @@ import unittest WORKFLOW = Path(__file__).resolve().parents[1] / ".gitea/workflows/deploy.yml" +DOCKERFILE = Path(__file__).resolve().parents[1] / "Dockerfile" +API_CLIENT = Path(__file__).resolve().parents[1] / "web/src/lib/api/client.ts" class DeployWorkflowContractTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.workflow = WORKFLOW.read_text(encoding="utf-8") + cls.dockerfile = DOCKERFILE.read_text(encoding="utf-8") + cls.api_client = API_CLIENT.read_text(encoding="utf-8") def test_targets_pro_branch_and_single_runner(self): self.assertIn("push:", self.workflow) @@ -35,8 +39,21 @@ class DeployWorkflowContractTest(unittest.TestCase): self.assertNotIn("git.118im.com", self.workflow) def test_uses_production_im_endpoint(self): + self.assertIn('TINODE_API_BASE="https://im-api.yuexuan.shop/v1/im"', self.workflow) self.assertIn('TINODE_FILE_BASE="https://im-api.yuexuan.shop/v0/file"', self.workflow) + def test_build_exposes_configured_api_base_to_vite(self): + self.assertIn("ARG TINODE_API_BASE=", self.dockerfile) + self.assertIn("ENV VITE_TINODE_API_BASE=$TINODE_API_BASE", self.dockerfile) + + def test_strips_accidentally_quoted_api_key_before_build(self): + self.assertIn('TINODE_API_KEY="${TINODE_API_KEY#\\\'}"', self.workflow) + self.assertIn('TINODE_API_KEY="${TINODE_API_KEY%\\\'}"', self.workflow) + + def test_api_client_prefers_the_configured_production_origin(self): + self.assertIn("import.meta.env.VITE_TINODE_API_BASE?.trim()", self.api_client) + self.assertIn("CONFIGURED_API_BASE || endpoints?.api || DEFAULT_API_BASE", self.api_client) + if __name__ == "__main__": unittest.main() diff --git a/web/src/lib/api/client.ts b/web/src/lib/api/client.ts index 5b01ea0b..9db1fbe1 100644 --- a/web/src/lib/api/client.ts +++ b/web/src/lib/api/client.ts @@ -12,6 +12,7 @@ import { useSessionStore } from '@/stores/session'; const DEFAULT_API_BASE = '/v1/im'; const DEFAULT_FILE_BASE = '/v0/file'; +const CONFIGURED_API_BASE = import.meta.env.VITE_TINODE_API_BASE?.trim(); const CONFIGURED_FILE_BASE = import.meta.env.VITE_TINODE_FILE_BASE?.trim(); export class ImApiError extends Error { @@ -119,7 +120,10 @@ export function requestId(): string { export function apiBase(): string { const endpoints = useSessionStore.getState().tenant?.endpoints; - return (endpoints?.api || DEFAULT_API_BASE).replace(/\/$/, ''); + // Production serves the static web app and IM API from different origins. The configured origin + // must also work before a tenant has been resolved, while local development keeps using Vite's + // same-origin proxy by leaving this value unset. + return (CONFIGURED_API_BASE || endpoints?.api || DEFAULT_API_BASE).replace(/\/$/, ''); } export function fileBase(): string { diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts index ece79680..65f8b57d 100644 --- a/web/src/vite-env.d.ts +++ b/web/src/vite-env.d.ts @@ -4,6 +4,7 @@ declare const __APP_VERSION__: string; interface ImportMetaEnv { + readonly VITE_TINODE_API_BASE?: string; readonly VITE_TINODE_API_KEY?: string; }