diff --git a/.env.githubci b/.env.githubci index 3d4c697c..82ec0a8e 100644 --- a/.env.githubci +++ b/.env.githubci @@ -72,6 +72,6 @@ OTEL_SERVICE_NAME=COOP_TEST_SERVICE NODE_ENV=CI ITEM_QUEUE_TRAFFIC_PERCENTAGE='0' -UI_URL=https://getcoop.com +UI_URL=http://localhost:3000 ENABLE_DEMO_REQUEST=false diff --git a/client/.env.example b/client/.env.example index 5d7ae634..85f8bdd6 100644 --- a/client/.env.example +++ b/client/.env.example @@ -16,7 +16,16 @@ # (e.g., to debug production errors). # GENERATE_SOURCEMAP=false -# URL of the content proxy service to use for content display in iframes. +# Public-facing base URL of this Coop instance (e.g. https://coop.example.com). +# Used to construct user-facing links such as password reset / invite links and +# API code samples. Leave unset to fall back to the browser's current origin. +VITE_UI_URL= + +# Base URL of the published Coop documentation. Override to point at a fork's +# or mirror's docs. +VITE_DOCS_URL=https://roostorg.github.io/coop/latest + +# URL of the content proxy service to use for content display in iframes. # This is used to proxy the content URL to the content proxy service. VITE_CONTENT_PROXY_URL=http://localhost:4000 diff --git a/client/src/lib/config.ts b/client/src/lib/config.ts new file mode 100644 index 00000000..f38aa1da --- /dev/null +++ b/client/src/lib/config.ts @@ -0,0 +1,31 @@ +/** + * Centralized URL configuration for the Coop client. + * + * Coop is open source and self-hosted, so any deployment-specific URL must come + * from the deployment's environment rather than being hardcoded. Add new URL + * constants here (sourced from `import.meta.env.VITE_*`) instead of inlining + * literals at call sites. + */ + +/** + * Base URL of this Coop instance, used to construct user-facing links such as + * password reset / invite links, dashboard deep-links, and API code samples. + * + * Configure with `VITE_UI_URL` at build time to override the runtime origin + * (useful when the client is served from a different host than its public + * URL, e.g. behind a reverse proxy). Falls back to the browser's current + * origin when unset. + */ +export const HOST_URL: string = + import.meta.env.VITE_UI_URL ?? window.location.origin; + +/** + * Base URL of the published Coop documentation site. + * + * Configure with `VITE_DOCS_URL` at build time to point at a fork's or + * mirror's docs. The default value lives in `client/.env.example` and is + * applied via the standard `.env` copy step; this code-level fallback only + * kicks in if the variable is missing entirely from the build environment. + */ +export const DOCS_URL: string = + import.meta.env.VITE_DOCS_URL ?? 'https://roostorg.github.io/coop/latest'; diff --git a/client/src/webpages/dashboard/actions/ActionForm.tsx b/client/src/webpages/dashboard/actions/ActionForm.tsx index 9ace988d..e07ddff2 100644 --- a/client/src/webpages/dashboard/actions/ActionForm.tsx +++ b/client/src/webpages/dashboard/actions/ActionForm.tsx @@ -1,3 +1,4 @@ +import { DOCS_URL } from '@/lib/config'; import { gql } from '@apollo/client'; import { Input, Select } from 'antd'; import Link from 'antd/lib/typography/Link'; @@ -322,7 +323,7 @@ export default function ActionForm() { Note: For each HTTP request we send to that URL, we will include a JSON body with information about the action. See the{' '} - + documentation {' '} for more information. diff --git a/client/src/webpages/dashboard/item_types/itemTypeCodeSampleUtils.ts b/client/src/webpages/dashboard/item_types/itemTypeCodeSampleUtils.ts index 0358d4dd..947f6dd1 100644 --- a/client/src/webpages/dashboard/item_types/itemTypeCodeSampleUtils.ts +++ b/client/src/webpages/dashboard/item_types/itemTypeCodeSampleUtils.ts @@ -1,3 +1,4 @@ +import { HOST_URL } from '@/lib/config'; import { ItemTypeKind } from '@roostorg/types'; import zipObject from 'lodash/zipObject'; @@ -163,13 +164,13 @@ function generateCurlRequest( switch (apiRoute) { case 'Items API': - return `curl --request POST --url https://getcoop.com/api/v1/items/async --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify( + return `curl --request POST --url ${HOST_URL}/api/v1/items/async --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify( data, null, 4, )}'`; case 'Reports API': - return `curl --request POST --url https://getcoop.com/api/v1/report --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify( + return `curl --request POST --url ${HOST_URL}/api/v1/report --header 'x-api-key: APIKEY' --header 'Content-Type: application/json' --data '${JSON.stringify( data, null, 4, @@ -197,7 +198,7 @@ data = ${JSON.stringify(data, null, 4) .replace(/true/g, 'True') .replace(/false/g, 'False')} response = requests.post( - 'https://getcoop.com/api/v1/items/async', + '${HOST_URL}/api/v1/items/async', headers=headers, json=data ) @@ -214,7 +215,7 @@ headers = { data = ${JSON.stringify(data, null, 4)} response = requests.post( - 'https://getcoop.com/api/v1/report', + '${HOST_URL}/api/v1/report', headers=headers, json=data ) @@ -236,7 +237,7 @@ function generateNodeJsRequest( ${JSON.stringify(data, null, 4)}; const response = await fetch( - "https://getcoop.com/api/v1/items/async", + "${HOST_URL}/api/v1/items/async", { method: 'post', body: JSON.stringify(body), @@ -253,7 +254,7 @@ console.log(response.status); return `const body = ${JSON.stringify(data, null, 4)} const response = await fetch( - "https://getcoop.com/api/v1/report", + "${HOST_URL}/api/v1/report", { method: 'post', body: JSON.stringify(body), @@ -291,7 +292,7 @@ $headers = [ $body = ${data}; -$response = $client->request('POST', 'https://getcoop.com/api/v1/items/async', [ +$response = $client->request('POST', '${HOST_URL}/api/v1/items/async', [ 'headers' => $headers, 'json' => $body, ]); @@ -312,7 +313,7 @@ $headers = [ $body = ${data}; -$response = $client->request('POST', 'https://getcoop.com/api/v1/report', [ +$response = $client->request('POST', '${HOST_URL}/api/v1/report', [ 'headers' => $headers, 'json' => $body, ]); diff --git a/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx b/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx index 39bb37bb..1e817f26 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewAppealSettings.tsx @@ -1,3 +1,4 @@ +import { DOCS_URL } from '@/lib/config'; import { gql } from '@apollo/client'; import { Input, notification } from 'antd'; import Link from 'antd/lib/typography/Link'; @@ -117,7 +118,7 @@ export default function ManualReviewAppealSettings() { Note: For each HTTP request we send to that URL, we will include a JSON body with information about the appeal. See the{' '} - + documentation {' '} for more information. diff --git a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx index 464c9774..556cfe69 100644 --- a/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx +++ b/client/src/webpages/dashboard/mrt/ManualReviewRecentDecisions.tsx @@ -1,6 +1,7 @@ import ChevronLeft from '@/icons/lni/Direction/chevron-left.svg?react'; import ChevronRight from '@/icons/lni/Direction/chevron-right.svg?react'; import CrossCircle from '@/icons/lni/Interface and Sign/cross-circle.svg?react'; +import { HOST_URL } from '@/lib/config'; import { RedoOutlined } from '@ant-design/icons'; import { gql } from '@apollo/client'; import { Button, Input } from 'antd'; @@ -576,7 +577,7 @@ export default function ManualReviewRecentDecisions() { item.reviewer, item.queue, item.createdAt, - `https://getcoop.com/dashboard/manual_review/recent?jobId=${item.jobId}`, + `${HOST_URL}/dashboard/manual_review/recent?jobId=${item.jobId}`, ]); // Combine the headers and rows into a CSV string @@ -623,7 +624,7 @@ export default function ManualReviewRecentDecisions() { getReviewerName(skip.userId), getQueueName(skip.queueId), parseDatetimeToReadableStringInUTC(new Date(skip.ts)), - `https://getcoop.com/dashboard/manual_review/recent?jobId=${skip.jobId}`, + `${HOST_URL}/dashboard/manual_review/recent?jobId=${skip.jobId}`, ]; }); // Define the CSV headers diff --git a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx index b31da1df..2d2173ac 100644 --- a/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx +++ b/client/src/webpages/dashboard/mrt/manual_review_job/IframeContentDisplayComponent.tsx @@ -15,10 +15,8 @@ export default function IframeContentDisplayComponent(props: { const { contentUrl } = props; const contentProxyUrl = - import.meta.env.VITE_CONTENT_PROXY_URL ?? - (import.meta.env.MODE === 'production' - ? 'https://content.getcoop.com' - : 'http://localhost:4000'); + import.meta.env.VITE_CONTENT_PROXY_URL?.trim() ?? + (import.meta.env.DEV ? 'http://localhost:4000' : window.location.origin); const [isIframeLoading, setIsIframeLoading] = useState(true); const [isTranslating, setIsTranslating] = useState(false); diff --git a/client/src/webpages/settings/ManageUsers.tsx b/client/src/webpages/settings/ManageUsers.tsx index 26f37307..67d5698b 100644 --- a/client/src/webpages/settings/ManageUsers.tsx +++ b/client/src/webpages/settings/ManageUsers.tsx @@ -1,3 +1,4 @@ +import { HOST_URL } from '@/lib/config'; import { gql } from '@apollo/client'; import { Select } from 'antd'; import { MouseEvent, useCallback, useMemo, useState } from 'react'; @@ -218,8 +219,7 @@ export default function ManageUsers() { const copyPasswordResetLink = () => { if (passwordResetToken) { - const uiUrl = import.meta.env.VITE_UI_URL ?? window.location.origin; - const resetUrl = `${uiUrl}/reset_password/${passwordResetToken}`; + const resetUrl = `${HOST_URL}/reset_password/${passwordResetToken}`; navigator.clipboard.writeText(resetUrl); setCopySuccess(true); setTimeout(() => setCopySuccess(false), 2000); @@ -542,9 +542,7 @@ export default function ManageUsers() { e.currentTarget.select()} /> diff --git a/client/src/webpages/settings/ManageUsersInviteUserSection.tsx b/client/src/webpages/settings/ManageUsersInviteUserSection.tsx index 8e6f2cba..390e760f 100644 --- a/client/src/webpages/settings/ManageUsersInviteUserSection.tsx +++ b/client/src/webpages/settings/ManageUsersInviteUserSection.tsx @@ -4,6 +4,7 @@ import { useGQLHasNcmecReportingEnabledQuery, useGQLInviteUserMutation, } from '@/graphql/generated'; +import { HOST_URL } from '@/lib/config'; import { titleCaseEnumString } from '@/utils/string'; import { gql } from '@apollo/client'; import { useState } from 'react'; @@ -78,8 +79,7 @@ export default function ManageUsersInviteUserSection() { const copyInviteLink = () => { if (inviteToken) { - const uiUrl = import.meta.env.VITE_UI_URL ?? window.location.origin; - const signupUrl = `${uiUrl}/signup/${inviteToken}`; + const signupUrl = `${HOST_URL}/signup/${inviteToken}`; navigator.clipboard.writeText(signupUrl); setCopySuccess(true); setTimeout(() => setCopySuccess(false), 2000); @@ -118,9 +118,7 @@ export default function ManageUsersInviteUserSection() { e.currentTarget.select()} /> diff --git a/client/src/webpages/settings/SSOSettings.tsx b/client/src/webpages/settings/SSOSettings.tsx index b1ee8ab9..b52d4975 100644 --- a/client/src/webpages/settings/SSOSettings.tsx +++ b/client/src/webpages/settings/SSOSettings.tsx @@ -5,6 +5,7 @@ import { Textarea } from '@/coop-ui/Textarea'; import { toast } from '@/coop-ui/Toast'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/coop-ui/Tooltip'; import { Heading, Text } from '@/coop-ui/Typography'; +import { HOST_URL } from '@/lib/config'; import { userHasPermissions } from '@/routing/permissions'; import { gql } from '@apollo/client'; import { Clipboard } from 'lucide-react'; @@ -76,7 +77,7 @@ export default function SSOSettings() { const copyText = (text: string) => { navigator.clipboard.writeText(text); }; - const callbackUri = `https://getcoop.com/api/v1/saml/login/${data?.myOrg?.id}/callback`; + const callbackUri = `${HOST_URL}/api/v1/saml/login/${data?.myOrg?.id}/callback`; return (