diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 627b058e..7ee88229 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -144,8 +144,6 @@ "expires_at": "Expires At", "when_this_api_key_will_expire": "When this API key will expire.", "optional_description_to_help_identify_this_keys_purpose": "Optional description to help identify this key's purpose.", - "name_must_be_at_least_3_characters": "Name must be at least 3 characters", - "name_cannot_exceed_50_characters": "Name cannot exceed 50 characters", "expiration_date_must_be_in_the_future": "Expiration date must be in the future", "revoke_api_key": "Revoke API Key", "never": "Never", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a1ecf447..58438ffd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "pocket-id-frontend", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pocket-id-frontend", - "version": "1.1.0", + "version": "1.2.0", "dependencies": { "@lucide/svelte": "^0.511.0", "@simplewebauthn/browser": "^13.1.0", @@ -16,9 +16,10 @@ "crypto": "^1.0.1", "jose": "^5.9.6", "qrcode": "^1.5.4", + "rollup-plugin-visualizer": "^6.0.1", "sveltekit-superforms": "^2.23.1", "tailwind-merge": "^3.3.0", - "zod": "^3.24.1" + "zod": "^3.25.55" }, "devDependencies": { "@inlang/paraglide-js": "^2.0.13", @@ -2412,6 +2413,15 @@ "node": ">=0.10.0" } }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2536,6 +2546,15 @@ "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", "optional": true }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3156,6 +3175,21 @@ "dev": true, "license": "MIT" }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3195,6 +3229,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3791,6 +3837,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4356,6 +4419,112 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-visualizer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.1.tgz", + "integrity": "sha512-NjlGElvLXCSZSAi3gNRZbfX3qlQbQcJ9TW97c5JpqfVwMhttj9YwEdPwcvbKj91RnMX2PWAjonvSEv6UEYtnRQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.0", + "picomatch": "^4.0.2", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "rolldown": "1.x", + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rolldown": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -5458,9 +5627,10 @@ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==" }, "node_modules/zod": { - "version": "3.24.1", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", - "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "version": "3.25.55", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.55.tgz", + "integrity": "sha512-219huNnkSLQnLsQ3uaRjXsxMrVm5C9W3OOpEVt2k5tvMKuA8nBSu38e0B//a+he9Iq2dvmk2VyYVlHqiHa4YBA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/package.json b/frontend/package.json index b0a6bb96..a3345055 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,7 +23,7 @@ "qrcode": "^1.5.4", "sveltekit-superforms": "^2.23.1", "tailwind-merge": "^3.3.0", - "zod": "^3.24.1" + "zod": "^3.25.55" }, "devDependencies": { "@inlang/paraglide-js": "^2.0.13", diff --git a/frontend/src/lib/stores/user-store.ts b/frontend/src/lib/stores/user-store.ts index 5f241f79..b0de4690 100644 --- a/frontend/src/lib/stores/user-store.ts +++ b/frontend/src/lib/stores/user-store.ts @@ -1,12 +1,12 @@ -import { setLocale } from '$lib/paraglide/runtime'; import type { User } from '$lib/types/user.type'; +import { setLocale } from '$lib/utils/locale.util'; import { writable } from 'svelte/store'; const userStore = writable(null); const setUser = (user: User) => { if (user.locale) { - setLocale(user.locale, { reload: false }); + setLocale(user.locale, false); } userStore.set(user); }; diff --git a/frontend/src/lib/types/oidc.type.ts b/frontend/src/lib/types/oidc.type.ts index fdaebd65..0a1a18c2 100644 --- a/frontend/src/lib/types/oidc.type.ts +++ b/frontend/src/lib/types/oidc.type.ts @@ -10,7 +10,7 @@ export type OidcClientFederatedIdentity = { issuer: string; subject?: string; audience?: string; - jwks?: string; + jwks: string | undefined; }; export type OidcClientCredentials = { @@ -18,7 +18,7 @@ export type OidcClientCredentials = { }; export type OidcClient = OidcClientMetaData & { - callbackURLs: string[]; // No longer requires at least one URL + callbackURLs: string[]; logoutCallbackURLs: string[]; isPublic: boolean; pkceEnabled: boolean; diff --git a/frontend/src/lib/utils/form-util.ts b/frontend/src/lib/utils/form-util.ts index 8eafee91..afeda980 100644 --- a/frontend/src/lib/utils/form-util.ts +++ b/frontend/src/lib/utils/form-util.ts @@ -1,5 +1,5 @@ -import { writable } from 'svelte/store'; -import { z } from 'zod'; +import { get, writable } from 'svelte/store'; +import { z } from 'zod/v4'; export type FormInput = { value: T; @@ -13,6 +13,7 @@ type FormInputs = { export function createForm>(schema: T, initialValues: z.infer) { // Create a writable store for the inputs const inputsStore = writable>>(initializeInputs(initialValues)); + const errorsStore = writable | undefined>(); function initializeInputs(initialValues: z.infer): FormInputs> { const inputs: FormInputs> = {} as FormInputs>; @@ -36,11 +37,12 @@ export function createForm>(schema: T, initialValu ); const result = schema.safeParse(values); + errorsStore.set(result.error); if (!result.success) { success = false; for (const input of Object.keys(inputs)) { - const error = result.error.errors.find((e) => e.path[0] === input); + const error = result.error.issues.find((e) => e.path[0] === input); if (error) { inputs[input as keyof z.infer].error = error.message; } else { @@ -58,15 +60,14 @@ export function createForm>(schema: T, initialValu } function data() { - let values: z.infer | null = null; - inputsStore.subscribe((inputs) => { - values = Object.fromEntries( - Object.entries(inputs).map(([key, input]) => { - input.value = trimValue(input.value); - return [key, input.value]; - }) - ) as z.infer; - })(); + const inputs = get(inputsStore); + + const values = Object.fromEntries( + Object.entries(inputs).map(([key, input]) => { + input.value = trimValue(input.value); + return [key, input.value]; + }) + ) as z.infer; return values; } @@ -108,6 +109,7 @@ export function createForm>(schema: T, initialValu return { schema, inputs: inputsStore, + errors: errorsStore, data, validate, setValue, diff --git a/frontend/src/lib/utils/locale.util.ts b/frontend/src/lib/utils/locale.util.ts new file mode 100644 index 00000000..54377433 --- /dev/null +++ b/frontend/src/lib/utils/locale.util.ts @@ -0,0 +1,10 @@ +import { setLocale as setParaglideLocale, type Locale } from '$lib/paraglide/runtime'; +import { z } from 'zod/v4'; + +export function setLocale(locale: Locale, reload = true) { + import(`../../../node_modules/zod/dist/esm/v4/locales/${locale}.js`) + .then((zodLocale) => z.config(zodLocale.default())) + .finally(() => { + setParaglideLocale(locale, { reload }); + }); +} diff --git a/frontend/src/routes/settings/account/account-form.svelte b/frontend/src/routes/settings/account/account-form.svelte index 3e634c3a..13441b2b 100644 --- a/frontend/src/routes/settings/account/account-form.svelte +++ b/frontend/src/routes/settings/account/account-form.svelte @@ -9,7 +9,7 @@ import { preventDefault } from '$lib/utils/event-util'; import { createForm } from '$lib/utils/form-util'; import { toast } from 'svelte-sonner'; - import { z } from 'zod'; + import { z } from 'zod/v4'; let { callback, @@ -35,7 +35,7 @@ .min(2) .max(30) .regex(/^[a-z0-9_@.-]+$/, m.username_can_only_contain()), - email: z.string().email(), + email: z.email(), isAdmin: z.boolean() }); type FormSchema = typeof formSchema; diff --git a/frontend/src/routes/settings/account/locale-picker.svelte b/frontend/src/routes/settings/account/locale-picker.svelte index 422132d7..428a12f8 100644 --- a/frontend/src/routes/settings/account/locale-picker.svelte +++ b/frontend/src/routes/settings/account/locale-picker.svelte @@ -1,8 +1,9 @@
@@ -76,8 +85,11 @@ placeholder="https://example.com/" value={identity.issuer} oninput={(e) => updateFederatedIdentity(i, 'issuer', e.currentTarget.value)} - required + aria-invalid={!!getFieldError(i, 'issuer')} /> + {#if getFieldError(i, 'issuer')} +

{getFieldError(i, 'issuer')}

+ {/if}
@@ -87,7 +99,11 @@ placeholder="Defaults to the client ID: {client?.id}" value={identity.subject || ''} oninput={(e) => updateFederatedIdentity(i, 'subject', e.currentTarget.value)} + aria-invalid={!!getFieldError(i, 'subject')} /> + {#if getFieldError(i, 'subject')} +

{getFieldError(i, 'subject')}

+ {/if}
@@ -97,7 +113,11 @@ placeholder="Defaults to the Pocket ID URL" value={identity.audience || ''} oninput={(e) => updateFederatedIdentity(i, 'audience', e.currentTarget.value)} + aria-invalid={!!getFieldError(i, 'audience')} /> + {#if getFieldError(i, 'audience')} +

{getFieldError(i, 'audience')}

+ {/if}
@@ -107,7 +127,11 @@ placeholder="Defaults to {identity.issuer || ''}/.well-known/jwks.json" value={identity.jwks || ''} oninput={(e) => updateFederatedIdentity(i, 'jwks', e.currentTarget.value)} + aria-invalid={!!getFieldError(i, 'jwks')} /> + {#if getFieldError(i, 'jwks')} +

{getFieldError(i, 'jwks')}

+ {/if}
@@ -115,10 +139,6 @@ - {#if error} -

{error}

- {/if} -