diff --git a/web/src/lib/subdomain.test.ts b/web/src/lib/subdomain.test.ts new file mode 100644 index 000000000..bb2b04485 --- /dev/null +++ b/web/src/lib/subdomain.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "vitest"; +import { isValidSubdomain, SUBDOMAIN_MAX_LENGTH, SUBDOMAIN_MIN_LENGTH } from "$lib/subdomain"; + +describe("isValidSubdomain", () => { + it.each(["team", "team-123", "a".repeat(SUBDOMAIN_MAX_LENGTH)])("accepts %j", (value) => { + expect(isValidSubdomain(value)).toBe(true); + }); + + it.each([ + "a".repeat(SUBDOMAIN_MIN_LENGTH - 1), + "a".repeat(SUBDOMAIN_MAX_LENGTH + 1), + "-team", + "team-", + "Team", + "team_name" + ])("rejects %j", (value) => { + expect(isValidSubdomain(value)).toBe(false); + }); +}); diff --git a/web/src/lib/subdomain.ts b/web/src/lib/subdomain.ts new file mode 100644 index 000000000..c980c00ac --- /dev/null +++ b/web/src/lib/subdomain.ts @@ -0,0 +1,8 @@ +export const SUBDOMAIN_MIN_LENGTH = 4; +export const SUBDOMAIN_MAX_LENGTH = 63; +export const SUBDOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$/; + +export const isValidSubdomain = (value: string): boolean => + value.length >= SUBDOMAIN_MIN_LENGTH && + value.length <= SUBDOMAIN_MAX_LENGTH && + SUBDOMAIN_PATTERN.test(value); diff --git a/web/src/routes/signup/verify/+page.svelte b/web/src/routes/signup/verify/+page.svelte index 36a0d7cc4..2422af3db 100644 --- a/web/src/routes/signup/verify/+page.svelte +++ b/web/src/routes/signup/verify/+page.svelte @@ -8,6 +8,12 @@ import Input from "$lib/components/ui/Input.svelte"; import ErrorAlert from "$lib/components/ui/Error.svelte"; import { pingIndexNow } from "$lib/api/indexnow"; + import { + isValidSubdomain, + SUBDOMAIN_MAX_LENGTH, + SUBDOMAIN_MIN_LENGTH, + SUBDOMAIN_PATTERN + } from "$lib/subdomain"; const appPath = (path: string) => resolve(path as "/"); @@ -15,15 +21,9 @@ const missingToken = $derived(!page.url.searchParams.get("token")); - const usernameRegex = /^[a-z0-9]([a-z0-9-]{2,61}[a-z0-9])?$/; let username = $state(""); let password = $state(""); - const valid = $derived( - username.length >= 4 && - username.length <= 63 && - usernameRegex.test(username) && - password.length >= 8 - ); + const valid = $derived(isValidSubdomain(username) && password.length >= 8); let done = $state<{ handle: string } | null>(null); @@ -112,8 +112,10 @@ autocapitalize="none" spellcheck="false" required + minlength={SUBDOMAIN_MIN_LENGTH} + maxlength={SUBDOMAIN_MAX_LENGTH} autocomplete="username" - pattern={usernameRegex.source} + pattern={SUBDOMAIN_PATTERN.source} title="Lowercase letters, digits, and hyphens; 4-63 characters; cannot start or end with a hyphen" error={!!submit.error} class="w-full"