Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
40 changes: 40 additions & 0 deletions .github/workflows/ci-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: ci-lint

on:
push:
branches:
- master
- develop
pull_request:
branches:
- master
- develop

permissions:
contents: read

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
cache: 'pnpm'

- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile --strict-peer-dependencies

- name: Run Linting and Formatting Check
run: pnpm run lint
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export default ts.config(
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
'no-undef': 'off',
'no-useless-assignment': 'warn',
'svelte/no-unused-svelte-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
},
},
{
Expand All @@ -53,6 +56,8 @@ export default ts.config(
'pnpm-lock.yaml',
'package-lock.json',
'yarn.lock',
'src/lib/api/internal',
'src/lib/components/ui',
],
}
);
2 changes: 1 addition & 1 deletion src/lib/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function GetBasePath(): string {
}

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

return `${url.origin}${pathname}`;
} catch (error) {
Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/Code.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import type { ChildrenFunc } from '$lib/types/ChildrenFunc';

interface Props {
children?: () => any;
children?: ChildrenFunc;
}

let { children }: Props = $props();
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/Container.svelte
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<script lang="ts">
import type { ChildrenFunc } from '$lib/types/ChildrenFunc';
import { cn } from '$lib/utils';

interface Props {
children: () => any;
children: ChildrenFunc;
class?: string;
}

Expand Down
1 change: 0 additions & 1 deletion src/lib/components/ControlModules/ControlListener.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<script lang="ts">
import type { ShockerResponse } from '$lib/api/internal/v1';
import { ControlType } from '$lib/signalr/models/ControlType';
import {
AddListener,
Expand All @@ -15,11 +14,11 @@
}

const id = $props.id();
let { shockerId, active = $bindable() }: Props = $props();

Check warning on line 17 in src/lib/components/ControlModules/ControlListener.svelte

View workflow job for this annotation

GitHub Actions / build-and-test

This assigned value is not used in subsequent statements

let timeoutHandle: TimeoutHandle | undefined;

const onEvent: ListenerSignature = (sid, controlType, duration, intensity) => {

Check warning on line 21 in src/lib/components/ControlModules/ControlListener.svelte

View workflow job for this annotation

GitHub Actions / build-and-test

'intensity' is defined but never used
clearTimeout(timeoutHandle);

active = controlType;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ChartNoAxesGantt, ClockFading, Gauge, Pause, Volume2, Waves, Zap } from '@lucide/svelte';
import { ClockFading, Gauge, Pause } from '@lucide/svelte';
import type { PublicShareShocker } from '$lib/api/internal/v1';
import {
ControlDurationDefault,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { ChartNoAxesGantt, ClockFading, Gauge, Pause } from '@lucide/svelte';
import { ClockFading, Gauge, Pause } from '@lucide/svelte';
import type { SharedShocker } from '$lib/api/internal/v1';
import {
ControlDurationDefault,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</script>

<div>
{#each Buttons as { type, Icon }}
{#each Buttons as { type, Icon } (type)}
{@const isDisabled = disabled || disabledControls[type]}
<button
class={cn(buttonClasses, {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/ControlModules/impl/ShockerMenu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { ShockerResponse } from '$lib/api/internal/v1';
import { Button } from '$lib/components/ui/button';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
import { resolve } from '$app/paths';

interface Props {
shocker: ShockerResponse;
Expand All @@ -12,7 +13,7 @@
let { shocker }: Props = $props();

function viewLogs() {
goto(`/shockers/logs/${shocker.id}`);
goto(resolve(`/shockers/logs/${shocker.id}`));
}
</script>

Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/FirmwareChannelSelector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@

<div class="flex flex-row items-center justify-start gap-2">
<ToggleGroup.Root type="single" bind:value={channel} {disabled}>
{#each FirmwareChannels as key}
{#each FirmwareChannels as key (key)}
<ToggleGroup.Item class="cursor-pointer capitalize" value={key} {disabled}>
{key}
</ToggleGroup.Item>
Expand Down
4 changes: 3 additions & 1 deletion src/lib/components/Keyboard.svelte
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<script lang="ts">
import type { ChildrenFunc } from '$lib/types/ChildrenFunc';

interface Props {
children?: () => any;
children?: ChildrenFunc;
}

let { children }: Props = $props();
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/LightSwitch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
<span class="sr-only">Toggle theme</span>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
{#each Object.values(ColorScheme) as value}
{#each Object.values(ColorScheme) as value (value)}
<DropdownMenu.Item
class="cursor-pointer capitalize"
onclick={() => evaluateLightSwitch(value)}>{value}</DropdownMenu.Item
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/confirm-dialog/dialog-manager.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { onMount } from 'svelte';
import DialogConfirm from './dialog-confirm.svelte';

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
let dialogData: ConfirmDialogContext<any> | null = $state(null);
let dialogCounter = $state(0);
let dialogOpen = $state(false);
Expand Down
7 changes: 1 addition & 6 deletions src/lib/components/datetime-picker/date-time-picker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,7 @@
</Popover.Trigger>
<Popover.Content bind:ref={contentRef} class="w-auto p-0">
<div class="flex border-b p-2">
<TimePicker
bind:time
setTime={(time) => {
time && setTime(time);
}}
/>
<TimePicker bind:time setTime={(time) => time && setTime(time)} />
</div>

<Calendar {onValueChange} type="single" bind:value={dateValue} />
Expand Down
3 changes: 2 additions & 1 deletion src/lib/components/datetime-picker/time-picker-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ export function getDateByType(time: Time, type: TimePickerType) {
return getValidMinuteOrSecond(String(time.second));
case 'hours':
return getValidHour(String(time.hour));
case '12hours':
case '12hours': {
const hours = display12HourValue(time.hour);
return getValid12Hour(String(hours));
}
default:
return '00';
}
Expand Down
3 changes: 3 additions & 0 deletions src/lib/components/input/TextInput.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script lang="ts">
/* eslint-disable svelte/no-navigation-without-resolve */

import { Input } from '$lib/components/ui/input';
import type { AnyComponent } from '$lib/types/AnyComponent';
import { GetValResColor, type ValidationResult } from '$lib/types/ValidationResult';
Comment on lines 1 to 6
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File-level /* eslint-disable svelte/no-navigation-without-resolve */ disables the rule for the entire component, but the only unresolved navigation is the external link in the validation message. Prefer an inline disable on that <a> so internal navigation linting remains effective for future changes.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -79,6 +81,7 @@
>
{validationResult.message}
{#if validationResult.link}
<!-- This must not be resolved, it in some cases points to external websites (HaveIBeenPwned) -->
<a
href={validationResult.link.href}
target="_blank"
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/metadata/OpenGraphTags.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
<meta property="og:locale" content={metaLocale} />
{/if}
{#if locales}
{#each locales as locale}
{#each locales as locale (locale)}
<meta property="og:locale:alternate" content={locale} />
{/each}
{/if}
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/shares/permission-switch.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import type { Component } from 'svelte';

interface Props {
/* eslint-disable-next-line @typescript-eslint/no-empty-object-type */
icon: Component<IconProps, {}, ''>;
enabled: boolean;
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/shares/user-selector.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
}

let userInput = $state('');
let { fetchedUser = $bindable(null) } = $props();
let { fetchedUser = $bindable(null) }: Props = $props();

function check(event: Event) {
event.preventDefault();
Expand Down
1 change: 0 additions & 1 deletion src/lib/components/utils/PauseToggle.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import type { BooleanLegacyDataResponse } from '$lib/api/internal/v1';
import { Button } from '$lib/components/ui/button';
import { toast } from 'svelte-sonner';
import LoadingCircle from '../svg/LoadingCircle.svelte';

interface Props {
shockerId: string;
Expand Down
9 changes: 5 additions & 4 deletions src/lib/inputvalidation/usernameValidator.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
/* eslint-disable no-control-regex */
/* eslint-disable no-misleading-character-class */

import { type UsernameCheckResponse } from '$lib/api/internal/v1';
import { UsernameAvailability } from '$lib/api/internal/v2';
import { isEmailAddress } from '$lib/inputvalidation/emailValidator';
import type { ValidationResult } from '$lib/types/ValidationResult';

/* eslint-disable no-misleading-character-class */

// This is taken from https://github.com/OpenShock/API/blob/develop/ServicesCommon/Validation/ChatsetMatchers.cs#L19
const MultipleWhiteSpaceRegex = /\s{2,}/;
// eslint-disable-next-line no-control-regex

// This is taken from https://github.com/OpenShock/API/blob/develop/Common/Validation/ChatsetMatchers.cs#L11-L41
const UnwantedCharacterRegex =
/[\u0000-\u001F\u007F-\u00A0\u02B0-\u036F\u1400-\u17FF\u1AB0-\u1AFF\u1DC0-\u1DFF\u2000-\u209F\u20D0-\u21FF\u2300-\u23FF\u2460-\u24FF\u25A0-\u27BF\u2900-\u297F\u2B00-\u2BFF\uFE00-\uFE0F\u{1F000}-\u{1F02F}\u{1F0A0}-\u{10FFFF}\u00AD\u180B-\u180F\u3000]/u;

Expand Down
2 changes: 1 addition & 1 deletion src/lib/metadata.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { asset } from '$app/paths';
import { PUBLIC_SITE_DESCRIPTION, PUBLIC_SITE_NAME } from '$env/static/public';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
function getPageTitleAndDescription(url: URL): { title: string; description: string } {
const title = PUBLIC_SITE_NAME.trim();
const details = PUBLIC_SITE_DESCRIPTION.trim();
Expand Down
4 changes: 2 additions & 2 deletions src/lib/sharelink-signalr/index.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ 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
Expand All @@ -58,8 +59,7 @@ export class ShareLinkSignalr {
});

// Look up in OpenShock API repository: Common/Hubs/IPublicShareHub.cs
connection.on('Welcome', (authType) => {
// Arg is the AuthType
connection.on('Welcome', (/* authType */) => {
this.signalr_state = HubConnectionState.Connected;
});

Expand Down
1 change: 1 addition & 0 deletions src/lib/stores/ConfirmDialogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ConfirmDialogContext<T> {
descSnippet?: Snippet<[T]>;
}

/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export const ConfirmDialogStore = writable<ConfirmDialogContext<any> | null>(null);

export function openConfirmDialog<T>(context: ConfirmDialogContext<T>) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/typeguards/propGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ export function HasStringArray<K extends string>(
return (
Object.hasOwn(obj, key) &&
Array.isArray(obj[key]) &&
obj[key].every((item: any) => isString(item))
obj[key].every((item: unknown) => isString(item))
);
}
3 changes: 2 additions & 1 deletion src/lib/types/AnyComponent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import type { Component, SvelteComponent } from 'svelte';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyComponent =
| Component<any, any, any>
| (new (...args: any[]) => SvelteComponent<any, any, any>);
2 changes: 2 additions & 0 deletions src/lib/types/ChildrenFunc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type ChildrenFunc = () => any;
54 changes: 54 additions & 0 deletions src/lib/utils/url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/* eslint-disable */

import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import type { Pathname } from '$app/types';
Comment on lines +1 to +6
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file starts with /* eslint-disable */, which disables all linting (including real bugs) for this module. Please replace this with targeted rule disables (or none) so CI lint checks still provide coverage for this security-sensitive redirect/navigation helper.

Copilot uses AI. Check for mistakes.

export function unsafeResolve(path: Pathname) {
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsafeResolve() unconditionally prefixes base, but many call sites (e.g. redirect query params built from page.url.pathname) already include the base path. This can produce broken URLs like ${base}${base}/... when deployed under a non-empty base path; consider making unsafeResolve detect already-based pathnames (or avoid using it on page.url.pathname).

Suggested change
export function unsafeResolve(path: Pathname) {
export function unsafeResolve(path: Pathname) {
// Avoid double-prefixing when `path` already includes `base`
if (!base || base === '/') {
return path;
}
if (path === base || path.startsWith(base + '/')) {
return path;
}

Copilot uses AI. Check for mistakes.
return base + path;
}

/**
* Validates a user-provided redirect target and guarantees a safe,
* same-origin pathname.
*
* - Absolute URLs are allowed only if they resolve to the current origin
* - External origins are rejected
* - Invalid URLs are rejected
*
* If the input is unsafe, the provided fallback pathname is returned.
*/
export function sanitizeRedirectPath(path: string, fallback: Pathname): Pathname {
try {
// Resolve relative or absolute paths against the current origin
const resolved = new URL(path, page.url.origin);

// Only allow same-origin navigation
if (resolved.origin === page.url.origin) {
return resolved.pathname as Pathname;
}
} catch {
// Treat malformed URLs as unsafe
}

return fallback;
}

/**
* Navigates to a redirect target taken from a query parameter,
* falling back to a safe internal pathname.
*
* This mirrors `$app/paths.resolve()` behavior for prefixing `base`,
* but avoids hash-based routing and strict typing issues.
*
* Typical use case:
* ?redirect=/account/settings
*/
export function gotoQueryRedirectOrFallback(fallback: Pathname, queryParam: string) {
const redirectTarget = page.url.searchParams.get(queryParam);

const target = redirectTarget ? sanitizeRedirectPath(redirectTarget, fallback) : fallback;

goto(unsafeResolve(target));
}
5 changes: 2 additions & 3 deletions src/routes/(anonymous)/+layout.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { UserStore } from '$lib/stores/UserStore';
import { gotoQueryRedirectOrFallback } from '$lib/utils/url';
import type { Snippet } from 'svelte';

let { children }: { children?: Snippet } = $props();

$effect(() => {
if (!$UserStore.loading && $UserStore.self) {
goto(page.url.searchParams.get('redirect') ?? '/home');
gotoQueryRedirectOrFallback('/home', 'redirect');
}
});
</script>
Expand Down
Loading
Loading