Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/hooks.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import { backendMetadata } from '$lib/state/BackendMetadata.svelte';
import { initializeDarkModeStore } from '$lib/stores/ColorSchemeStore.svelte';
import { initializeSerialPortsStore } from '$lib/stores/SerialPortsStore';
import { UserStore } from '$lib/stores/UserStore';
import { sanitizeRedirectSearchParam } from '$lib/utils/url';

export async function init() {
sanitizeRedirectSearchParam();

initBackendMetadata().catch((error) => {
handleApiError(error);
});
Expand Down
18 changes: 8 additions & 10 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PUBLIC_BACKEND_API_URL } from '$env/static/public';
import { getBackendURL } from '$lib/utils/url';
import {
APITokensApi,
AccountApi as AccountV1Api,
Expand Down Expand Up @@ -27,22 +28,19 @@ function GetBasePath(): string {
}

try {
const url = new URL(PUBLIC_BACKEND_API_URL);
const url = getBackendURL();

if (url.protocol !== 'https:') {
throw new Error('PUBLIC_BACKEND_API_URL must be a HTTPS url');
if (url.pathname === '/') {
return url.origin;
}

if (url.search || url.hash) {
throw new Error('PUBLIC_BACKEND_API_URL must not contain query parameters or hash');
}

// Normalize pathname
const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
// Trim trailing slashes
const pathname = url.pathname.replace(/\/+$/, '');

return `${url.origin}${pathname}`;
} catch (error) {
throw new Error('PUBLIC_BACKEND_API_URL is not a valid URL', { cause: error });
const message = error instanceof Error ? error.message : String(error);
throw new Error(`PUBLIC_BACKEND_API_URL is not a valid URL: ${message}`, { cause: error });
}
}

Expand Down
18 changes: 5 additions & 13 deletions src/lib/api/next/base.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { PUBLIC_BACKEND_API_URL } from '$env/static/public';
import { getBackendURL, type BackendPath } from '$lib/utils/url';
import { ResponseError } from './ResponseError';

type ApiVersion = 1 | 2;
export type Path = `${ApiVersion}` | `${ApiVersion}/${string}`;

export async function GetJson<T>(
path: Path,
path: BackendPath,
expectedStatus = 200,
transformer: (data: unknown) => T
): Promise<T> {
const res = await fetch(GetBackendUrl(path), {
const res = await fetch(getBackendURL(path), {
method: 'GET',
headers: { accept: 'application/json' },
credentials: 'include',
Expand All @@ -30,18 +27,13 @@ export async function GetJson<T>(
return transformer(data);
}

export function GetBackendUrl(path: Path): URL {
const url = new URL(path, PUBLIC_BACKEND_API_URL);
return url;
}

export async function PostJson<T>(
path: Path,
path: BackendPath,
body: unknown,
expectedStatus = 200,
transformer: (data: unknown) => T
): Promise<T> {
const url = GetBackendUrl(path);
const url = getBackendURL(path);

const res = await fetch(url, {
method: 'POST',
Expand Down
22 changes: 14 additions & 8 deletions src/lib/api/next/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { GetBackendUrl, GetJson, PostJson } from './base';
import { getBackendURL } from '$lib/utils/url';
import { GetJson, PostJson } from './base';
import type { LoginOkResponse, OAuthFinalizeRequest, OAuthSignupData } from './models';
import { TransformLoginOkResponse, TransformOAuthSignupData } from './transformers';

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}`).toString();
const url = getBackendURL(`1/oauth/${encodeURIComponent(provider)}/authorize`);

url.searchParams.set('flow', flow);

return url.href;
}

export async function OAuthSignupGetData(provider: string) {
const providerEnc = encodeURIComponent(provider);
return GetJson<OAuthSignupData>(
`1/oauth/${providerEnc}/signup-data`,
`1/oauth/${encodeURIComponent(provider)}/signup-data`,
200,
TransformOAuthSignupData
);
Expand All @@ -21,6 +23,10 @@ export async function OAuthSignupFinalize(
provider: string,
payload: OAuthFinalizeRequest
): Promise<LoginOkResponse> {
const providerEnc = encodeURIComponent(provider);
return PostJson(`1/oauth/${providerEnc}/signup-finalize`, payload, 200, TransformLoginOkResponse);
return PostJson(
`1/oauth/${encodeURIComponent(provider)}/signup-finalize`,
payload,
200,
TransformLoginOkResponse
);
}
6 changes: 4 additions & 2 deletions src/lib/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { asset } from '$app/paths';
import { PUBLIC_SITE_DESCRIPTION, PUBLIC_SITE_NAME } from '$env/static/public';
import { getSiteAssetURL } from './utils/url';

const LogoSvgAssetURL = getSiteAssetURL('/logo.svg');

/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function getPageTitleAndDescription(url: URL): { title: string; description: string } {
Expand All @@ -23,7 +25,7 @@ export function buildMetaData(url: URL) {
const { title, description } = getPageTitleAndDescription(url);

const image = {
src: new URL(asset('/logo.svg'), url.origin).href,
src: LogoSvgAssetURL.href,
alt: 'OpenShock Logo',
};

Expand Down
25 changes: 13 additions & 12 deletions src/lib/sharelink-signalr/index.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dev } from '$app/environment';
import { PUBLIC_BACKEND_API_URL } from '$env/static/public';
import type { Control } from '$lib/signalr/models/Control';
import { getBackendURL } from '$lib/utils/url';
import {
HttpTransportType,
type HubConnection,
Expand All @@ -10,6 +10,14 @@ import {
} from '@microsoft/signalr';
import { toast } from 'svelte-sonner';

function GetShareLinkURL(shareLinkId: string, customName: string | null) {
const url = getBackendURL(`1/hubs/share/link/${encodeURIComponent(shareLinkId)}`);
if (customName !== null) {
url.searchParams.set('name', customName || '');
}
return url.href;
}

export class ShareLinkSignalr {
readonly shareLinkId: string;
readonly customName: string | null;
Expand All @@ -32,17 +40,10 @@ export class ShareLinkSignalr {

const connection = new HubConnectionBuilder()
.configureLogging(dev ? LogLevel.Debug : LogLevel.Warning)
.withUrl(
/* eslint-disable-next-line svelte/prefer-svelte-reactivity */
new URL(
`1/hubs/share/link/${this.shareLinkId}?name=${this.customName}`,
PUBLIC_BACKEND_API_URL
).toString(),
{
transport: HttpTransportType.WebSockets,
skipNegotiation: true,
}
)
.withUrl(GetShareLinkURL(this.shareLinkId, this.customName), {
transport: HttpTransportType.WebSockets,
skipNegotiation: true,
})
.withAutomaticReconnect([0, 1000, 2000, 5000, 10000, 10000, 15000, 30000, 60000])
.build();

Expand Down
6 changes: 4 additions & 2 deletions src/lib/signalr/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { dev } from '$app/environment';
import { PUBLIC_BACKEND_API_URL } from '$env/static/public';
import { getBackendURL } from '$lib/utils/url';
import {
HttpTransportType,
type HubConnection,
Expand All @@ -20,6 +20,8 @@ import {
handleSignalrOtaRollback,
} from './handlers';

const BackendHubUserUrl = getBackendURL('1/hubs/user').href;

const signalr_connection = writable<HubConnection | null>(null);
const signalr_state = writable<HubConnectionState>(HubConnectionState.Disconnected);

Expand All @@ -31,7 +33,7 @@ export async function initializeSignalR() {

connection = new HubConnectionBuilder()
.configureLogging(dev ? LogLevel.Debug : LogLevel.Warning)
.withUrl(new URL(`1/hubs/user`, PUBLIC_BACKEND_API_URL).toString(), {
.withUrl(BackendHubUserUrl, {
transport: HttpTransportType.WebSockets,
skipNegotiation: true,
})
Expand Down
14 changes: 14 additions & 0 deletions src/lib/state/RedirectSanitized.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/** Reactive flag set when {@link sanitizeRedirectSearchParam} strips a malicious redirect param. */
let flag = $state(false);

export const redirectSanitized = {
get value() {
return flag;
},
set() {
flag = true;
},
reset() {
flag = false;
},
};
Loading
Loading