diff --git a/.env.development b/.env.development index 20998c9e..15730ea9 100644 --- a/.env.development +++ b/.env.development @@ -1,7 +1,7 @@ -PUBLIC_SITE_DOMAIN=openshock.dev -PUBLIC_SITE_SHORT_DOMAIN=openshock.dev -PUBLIC_BACKEND_API_DOMAIN=api.openshock.dev -PUBLIC_GATEWAY_CSP_WILDCARD=*.openshock.dev +PUBLIC_SITE_URL=https://openshock.dev +PUBLIC_SITE_SHORT_URL=https://openshock.dev +PUBLIC_BACKEND_API_URL=https://api.openshock.dev +PUBLIC_GATEWAY_CSP_WILDCARD=https://*.openshock.dev PUBLIC_TURNSTILE_DEV_BYPASS_VALUE=dev-bypass PUBLIC_DEVELOPMENT_BANNER=true \ No newline at end of file diff --git a/.env.production b/.env.production index 3c171d7c..f239c486 100644 --- a/.env.production +++ b/.env.production @@ -1,4 +1,4 @@ -PUBLIC_SITE_DOMAIN=openshock.app -PUBLIC_SITE_SHORT_DOMAIN=openshock.app -PUBLIC_BACKEND_API_DOMAIN=api.openshock.app -PUBLIC_GATEWAY_CSP_WILDCARD=*.openshock.app \ No newline at end of file +PUBLIC_SITE_URL=https://openshock.app +PUBLIC_SITE_SHORT_URL=https://openshock.app +PUBLIC_BACKEND_API_URL=https://api.openshock.app +PUBLIC_GATEWAY_CSP_WILDCARD=https://*.openshock.app \ No newline at end of file diff --git a/.env.test b/.env.test index b26f5310..0c805822 100644 --- a/.env.test +++ b/.env.test @@ -1,6 +1,6 @@ -PUBLIC_SITE_DOMAIN=openshock.dev -PUBLIC_SITE_SHORT_DOMAIN=openshock.dev -PUBLIC_BACKEND_API_DOMAIN=api.openshock.dev -PUBLIC_GATEWAY_CSP_WILDCARD=*.openshock.dev +PUBLIC_SITE_URL=https://openshock.dev +PUBLIC_SITE_SHORT_URL=https://openshock.dev +PUBLIC_BACKEND_API_URL=https://api.openshock.dev +PUBLIC_GATEWAY_CSP_WILDCARD=https://*.openshock.dev PUBLIC_TURNSTILE_DEV_BYPASS_VALUE=dev-bypass diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 46202c87..c85d0ee0 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,4 +1,5 @@ -import { PUBLIC_BACKEND_API_DOMAIN } from '$env/static/public'; +import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; + import { APITokensApi, AccountApi as AccountV1Api, @@ -22,17 +23,21 @@ import { } from './internal/v2'; function GetBasePath() { - const domain = (PUBLIC_BACKEND_API_DOMAIN || undefined) as string | undefined; - - if (!domain) { - return undefined; + if (!PUBLIC_BACKEND_API_URL) { + throw new Error('PUBLIC_BACKEND_API_URL is not set in the environment'); } - if (!/^[a-z0-9.-]+$/i.test(domain)) { - return undefined; + try { + const parsedUrl = new URL(PUBLIC_BACKEND_API_URL); + // Remove trailing slash unless the path is just "/" + let basePath = parsedUrl.toString(); + if (basePath.endsWith('/') && basePath.length > parsedUrl.origin.length + 1) { + basePath = basePath.slice(0, -1); + } + return basePath; + } catch (error: any) { + throw new Error('PUBLIC_BACKEND_API_URL is not a valid URL', { cause: error }); } - - return 'https://' + domain; // TODO: Add configurable protocol } function GetConfig(): ConfigurationParameters { diff --git a/src/lib/api/next/base.ts b/src/lib/api/next/base.ts index 87b95af7..b327c66b 100644 --- a/src/lib/api/next/base.ts +++ b/src/lib/api/next/base.ts @@ -1,21 +1,15 @@ -import { PUBLIC_BACKEND_API_DOMAIN } from '$env/static/public'; +import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; import { ResponseError } from './ResponseError'; -const BaseUrl = `https://${PUBLIC_BACKEND_API_DOMAIN}`; - type ApiVersion = 1 | 2; -export type Path = `/${ApiVersion}/${string}`; - -export function GetBackendUrl(path: Path) { - return BaseUrl + path; -} +export type Path = `${ApiVersion}/${string}`; export async function GetJson( path: Path, expectedStatus = 200, transformer: (data: unknown) => T ): Promise { - const res = await fetch(BaseUrl + path, { + const res = await fetch(GetBackendUrl(path), { method: 'GET', headers: { accept: 'application/json' }, credentials: 'include', @@ -36,6 +30,11 @@ export async function GetJson( return transformer(data); } +export function GetBackendUrl(path: Path): URL { + const url = new URL(path, PUBLIC_BACKEND_API_URL); + return url; +} + export async function PostJson( path: Path, body: unknown, diff --git a/src/lib/api/next/oauth.ts b/src/lib/api/next/oauth.ts index 0a31f50e..75666d94 100644 --- a/src/lib/api/next/oauth.ts +++ b/src/lib/api/next/oauth.ts @@ -2,16 +2,16 @@ import { GetBackendUrl, GetJson, PostJson } from './base'; import type { LoginOkResponse, OAuthFinalizeRequest, OAuthSignupData } from './models'; import { TransformLoginOkResponse, TransformOAuthSignupData } from './transformers'; -export function GetOAuthAuthorizeUrl(provider: string, flow: 'LoginOrCreate' | 'Link') { +export function GetOAuthAuthorizeUrl(provider: string, flow: 'LoginOrCreate' | 'Link'): string { const providerEnc = encodeURIComponent(provider); const flowEnc = encodeURIComponent(flow); - return GetBackendUrl(`/1/oauth/${providerEnc}/authorize?flow=${flowEnc}`); + return GetBackendUrl(`1/oauth/${providerEnc}/authorize?flow=${flowEnc}`).toString(); } export async function OAuthSignupGetData(provider: string) { const providerEnc = encodeURIComponent(provider); return GetJson( - `/1/oauth/${providerEnc}/signup-data`, + `1/oauth/${providerEnc}/signup-data`, 200, TransformOAuthSignupData ); @@ -23,7 +23,7 @@ export async function OAuthSignupFinalize( ): Promise { const providerEnc = encodeURIComponent(provider); return PostJson( - `/1/oauth/${providerEnc}/signup-finalize`, + `1/oauth/${providerEnc}/signup-finalize`, payload, 200, TransformLoginOkResponse diff --git a/src/lib/signalr/index.ts b/src/lib/signalr/index.ts index 0256666b..609ab906 100644 --- a/src/lib/signalr/index.ts +++ b/src/lib/signalr/index.ts @@ -6,7 +6,7 @@ import { LogLevel, } from '@microsoft/signalr'; import { dev } from '$app/environment'; -import { PUBLIC_BACKEND_API_DOMAIN } from '$env/static/public'; +import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; import { toast } from 'svelte-sonner'; import { type Readable, get, writable } from 'svelte/store'; import { @@ -31,7 +31,7 @@ export async function initializeSignalR() { connection = new HubConnectionBuilder() .configureLogging(dev ? LogLevel.Debug : LogLevel.Warning) - .withUrl(`https://${PUBLIC_BACKEND_API_DOMAIN}/1/hubs/user`, { + .withUrl(new URL(`1/hubs/user`, PUBLIC_BACKEND_API_URL).toString(), { transport: HttpTransportType.WebSockets, skipNegotiation: true, }) diff --git a/src/routes/(anonymous)/+page.server.ts b/src/routes/(anonymous)/+page.server.ts index d4d2b799..a785b337 100644 --- a/src/routes/(anonymous)/+page.server.ts +++ b/src/routes/(anonymous)/+page.server.ts @@ -1,4 +1,4 @@ -import { PUBLIC_BACKEND_API_DOMAIN } from '$env/static/public'; +import { PUBLIC_BACKEND_API_URL } from '$env/static/public'; import { Configuration, MetaApi } from '$lib/api/internal/v1'; type ResponseType = Promise<{ ok: false; error: string } | { ok: true; deviceCount: number }>; @@ -16,7 +16,7 @@ export async function load({ setHeaders }): ResponseType { try { const metaApi = new MetaApi( new Configuration({ - basePath: 'https://' + PUBLIC_BACKEND_API_DOMAIN, + basePath: PUBLIC_BACKEND_API_URL, headers: { 'User-Agent': 'OpenShockFrontend/1.0 (ServerSide)', }, diff --git a/svelte.config.js b/svelte.config.js index 332ab793..51694951 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -31,6 +31,28 @@ function getGitBranch() { return child_process.execSync('git rev-parse --abbrev-ref HEAD').toString().trim(); } +function getWsUrlFromHttpUrl(url) { + if (url.startsWith('https://')) { + return url.replace('https://', 'wss://'); + } else if (url.startsWith('http://')) { + return url.replace('http://', 'ws://'); + } + + throw new Error(`Invalid URL was provided, it must start with http:// or https:// [${url}]`); +} + +function getSvelteBasePath() { + try { + const url = new URL(dotenv.PUBLIC_SITE_URL); + const path = url.pathname === '/' ? '' : url.pathname; + console.log(`Using base path: [${path}] from PUBLIC_SITE_URL: ${dotenv.PUBLIC_SITE_URL}`); + return path; + } catch (error) { + throw new Error(`PUBLIC_SITE_URL is not a valid URL: ${error.message}`, { cause: error }); + } + +} + const commitHash = getGitHash(); const branchName = getGitBranch(); @@ -49,6 +71,9 @@ const config = { }, kit: { adapter: adapter(), + paths: { + base: getSvelteBasePath() + }, csp: { mode: 'hash', directives: { @@ -65,9 +90,10 @@ const config = { ], 'connect-src': [ 'self', - 'https://' + dotenv.PUBLIC_BACKEND_API_DOMAIN, - 'wss://' + dotenv.PUBLIC_BACKEND_API_DOMAIN, - 'wss://' + dotenv.PUBLIC_GATEWAY_CSP_WILDCARD, + dotenv.PUBLIC_BACKEND_API_URL, + getWsUrlFromHttpUrl(dotenv.PUBLIC_BACKEND_API_URL), + dotenv.PUBLIC_GATEWAY_CSP_WILDCARD, + getWsUrlFromHttpUrl(dotenv.PUBLIC_GATEWAY_CSP_WILDCARD), 'https://firmware.openshock.org', 'https://api.pwnedpasswords.com/range/', 'https://cloudflareinsights.com', diff --git a/vite.config.ts b/vite.config.ts index f870ac22..29f12e01 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -66,17 +66,16 @@ function getPlugins(useLocalRedirect: boolean): PluginOption[] { } async function getServerConfig(mode: string, useLocalRedirect: boolean) { - if (!useLocalRedirect) return undefined; - const vars = { ...env, ...loadEnv(mode, process.cwd(), ['PUBLIC_']) }; - const domain = vars.PUBLIC_SITE_DOMAIN; - - // Load environment variables - if (!domain) { - printError('PUBLIC_SITE_DOMAIN must be set in your environment'); + if(!vars.PUBLIC_SITE_URL) { + printError('PUBLIC_SITE_URL must be set in your environment'); process.exit(1); } + if (!useLocalRedirect) return undefined; + + const domain = new URL(vars.PUBLIC_SITE_URL).hostname; + if (domain === 'localhost') { return { host: 'localhost', port: 8080, proxy: {} }; } @@ -93,7 +92,7 @@ export default defineConfig(async ({ command, mode, isPreview }) => { const isLocalServe = command === 'serve' || isPreview === true; const isProduction = mode === 'production' && (isTruthy(env.DOCKER) || isTruthy(env.CF_PAGES)); - // If we are running locally, ensure that local.{PUBLIC_SITE_DOMAIN} resolves to localhost, and then use mkcert to generate a certificate + // If we are running locally, ensure that local.{PUBLIC_SITE_URL} resolves to localhost, and then use mkcert to generate a certificate const useLocalRedirect = isLocalServe && !isProduction && !isTruthy(env.CI); return defineConfig({