diff --git a/apps/publisher-dashboard/package.json b/apps/publisher-dashboard/package.json index 90172b9..c52025c 100644 --- a/apps/publisher-dashboard/package.json +++ b/apps/publisher-dashboard/package.json @@ -15,13 +15,18 @@ "@orpc/client": "^1.13.2", "@orpc/contract": "^1.13.2", "@reviq/api-contract": "workspace:*", + "@simplewebauthn/browser": "^13.2.2", "@tanstack/svelte-query": "^6.0.14", "@tanstack/svelte-query-devtools": "^6.0.3", "bits-ui": "^2.15.4", "clsx": "^2.1.1", + "libphonenumber-js": "^1.12.33", + "svelte-sonner": "^1.0.7", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2", - "tslib": "catalog:" + "tslib": "catalog:", + "ua-parser-js": "^2.0.7", + "zxcvbn": "^4.4.2" }, "devDependencies": { "@internationalized/date": "^3.10.1", @@ -32,6 +37,8 @@ "@sveltejs/kit": "^2.49.4", "@sveltejs/vite-plugin-svelte": "^6.2.3", "@tailwindcss/vite": "^4.1.4", + "@types/ua-parser-js": "^0.7.39", + "@types/zxcvbn": "^4.4.5", "eslint": "catalog:", "svelte": "^5.28.2", "svelte-check": "^4.2.1", diff --git a/apps/publisher-dashboard/src/lib/components/auth/error-alert.svelte b/apps/publisher-dashboard/src/lib/components/auth/error-alert.svelte new file mode 100644 index 0000000..2e124ba --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/auth/error-alert.svelte @@ -0,0 +1,19 @@ + + +{#if message} +
+ + + {message} + +
+{/if} diff --git a/apps/publisher-dashboard/src/lib/components/auth/index.ts b/apps/publisher-dashboard/src/lib/components/auth/index.ts new file mode 100644 index 0000000..75f95c5 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/auth/index.ts @@ -0,0 +1,4 @@ +export { default as ErrorAlert } from "./error-alert.svelte"; +export { default as PasswordFormField } from "./password-form-field.svelte"; +export { default as PasswordInput } from "./password-input.svelte"; +export { default as PasswordStrength } from "./password-strength.svelte"; diff --git a/apps/publisher-dashboard/src/lib/components/auth/password-form-field.svelte b/apps/publisher-dashboard/src/lib/components/auth/password-form-field.svelte new file mode 100644 index 0000000..5d09bea --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/auth/password-form-field.svelte @@ -0,0 +1,54 @@ + + +
+ + + {#if showStrength} + + {/if} + {#if error} +

{error}

+ {/if} +
diff --git a/apps/publisher-dashboard/src/lib/components/auth/password-input.svelte b/apps/publisher-dashboard/src/lib/components/auth/password-input.svelte new file mode 100644 index 0000000..2fedbe3 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/auth/password-input.svelte @@ -0,0 +1,64 @@ + + +
+ + +
diff --git a/apps/publisher-dashboard/src/lib/components/auth/password-strength.svelte b/apps/publisher-dashboard/src/lib/components/auth/password-strength.svelte new file mode 100644 index 0000000..c4b3510 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/auth/password-strength.svelte @@ -0,0 +1,61 @@ + + +{#if password} +
+ +
+ {#each Array(4) as _, i} +
+ {/each} +
+ + +

+ Password strength: {config.label} +

+ + + {#if result?.feedback.warning || result?.feedback.suggestions.length} +
+ {#if result.feedback.warning} +

{result.feedback.warning}

+ {/if} + {#each result.feedback.suggestions as suggestion} +

{suggestion}

+ {/each} +
+ {/if} +
+{/if} diff --git a/apps/publisher-dashboard/src/lib/components/ui/alert/alert-description.svelte b/apps/publisher-dashboard/src/lib/components/ui/alert/alert-description.svelte new file mode 100644 index 0000000..c74e5ed --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/alert/alert-description.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/alert/alert-title.svelte b/apps/publisher-dashboard/src/lib/components/ui/alert/alert-title.svelte new file mode 100644 index 0000000..84768ce --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/alert/alert-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/apps/publisher-dashboard/src/lib/components/ui/alert/alert.svelte b/apps/publisher-dashboard/src/lib/components/ui/alert/alert.svelte new file mode 100644 index 0000000..1ea3e86 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/alert/alert.svelte @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/publisher-dashboard/src/lib/components/ui/alert/index.ts b/apps/publisher-dashboard/src/lib/components/ui/alert/index.ts new file mode 100644 index 0000000..4f66a77 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/alert/index.ts @@ -0,0 +1,15 @@ +import Root from "./alert.svelte"; +import Description from "./alert-description.svelte"; +import Title from "./alert-title.svelte"; + +export { type AlertVariant, alertVariants } from "./alert.svelte"; + +export { + Root, + Description, + Title, + // + Root as Alert, + Description as AlertDescription, + Title as AlertTitle, +}; diff --git a/apps/publisher-dashboard/src/lib/components/ui/loading-button/index.ts b/apps/publisher-dashboard/src/lib/components/ui/loading-button/index.ts new file mode 100644 index 0000000..7cfbfe5 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/loading-button/index.ts @@ -0,0 +1,3 @@ +import Root from "./loading-button.svelte"; + +export { Root, Root as LoadingButton }; diff --git a/apps/publisher-dashboard/src/lib/components/ui/loading-button/loading-button.svelte b/apps/publisher-dashboard/src/lib/components/ui/loading-button/loading-button.svelte new file mode 100644 index 0000000..c484912 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/components/ui/loading-button/loading-button.svelte @@ -0,0 +1,28 @@ + + + diff --git a/apps/publisher-dashboard/src/lib/stores/auth.svelte.ts b/apps/publisher-dashboard/src/lib/stores/auth.svelte.ts new file mode 100644 index 0000000..619005f --- /dev/null +++ b/apps/publisher-dashboard/src/lib/stores/auth.svelte.ts @@ -0,0 +1,76 @@ +/** + * Auth Store - Svelte 5 runes-based store for authentication flow state + * + * This module provides state management for the multi-step login flow. + * State persists across page navigations in SPA mode. + */ + +interface LoginFlowState { + email: string | null; + hasPasskey: boolean; + hasPassword: boolean; + isTrustedDevice: boolean; +} + +/** + * Module-level reactive state for login flow + * Persists across page navigations within the SPA + */ +export const loginFlowState: LoginFlowState = $state({ + email: null, + hasPasskey: false, + hasPassword: false, + isTrustedDevice: false, +}); + +/** + * Set login flow state after createLoginRequest API call + * Call this before navigating to passkey/password/confirm pages + */ +export function setLoginFlowState(response: { + email: string; + hasPasskey: boolean; + hasPassword: boolean; + isTrustedDevice: boolean; +}): void { + loginFlowState.email = response.email; + loginFlowState.hasPasskey = response.hasPasskey; + loginFlowState.hasPassword = response.hasPassword; + loginFlowState.isTrustedDevice = response.isTrustedDevice; +} + +/** + * Clear login flow state after successful login or when user navigates away + */ +export function clearLoginFlowState(): void { + loginFlowState.email = null; + loginFlowState.hasPasskey = false; + loginFlowState.hasPassword = false; + loginFlowState.isTrustedDevice = false; +} + +/** + * Check if there is an active login flow + * Used for route guards on passkey/password/confirm pages + */ +export function hasActiveLoginFlow(): boolean { + return loginFlowState.email !== null; +} + +/** + * Get masked email for display (e.g., "j***@example.com") + */ +export function getMaskedEmail(): string { + if (!loginFlowState.email) { + return ""; + } + + const [local, domain] = loginFlowState.email.split("@"); + if (!domain) { + return loginFlowState.email; + } + + const maskedLocal = local.length > 1 ? `${local[0]}***` : `${local}***`; + + return `${maskedLocal}@${domain}`; +} diff --git a/apps/publisher-dashboard/src/lib/utils/validation.ts b/apps/publisher-dashboard/src/lib/utils/validation.ts new file mode 100644 index 0000000..82d9ae4 --- /dev/null +++ b/apps/publisher-dashboard/src/lib/utils/validation.ts @@ -0,0 +1,62 @@ +import { + isValidPhoneNumber, + parsePhoneNumberWithError, +} from "libphonenumber-js"; + +/** + * Validates email format using a simple regex pattern. + * Matches the backend validation pattern. + */ +export function isValidEmail(email: string): boolean { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); +} + +/** + * Validates and formats phone numbers using libphonenumber-js. + * Returns validation status and optionally the formatted number. + */ +export function validatePhone(value: string): { + valid: boolean; + formatted?: string; +} { + if (!value) { + return { valid: true }; + } + try { + const phone = parsePhoneNumberWithError(value); + if (isValidPhoneNumber(phone.number)) { + return { valid: true, formatted: phone.formatInternational() }; + } + return { valid: false }; + } catch { + return { valid: false }; + } +} + +/** + * Masks an email for display, e.g., "john@example.com" -> "j***@example.com" + */ +export function maskEmail(email: string): string { + const [local, domain] = email.split("@"); + if (!domain) { + return email; + } + const masked = + local.length > 1 + ? `${local[0]}${"*".repeat(Math.min(local.length - 1, 3))}` + : local; + return `${masked}@${domain}`; +} + +/** + * Parses an error to extract a user-friendly message. + */ +export function parseErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + return "An unexpected error occurred"; +} diff --git a/apps/publisher-dashboard/src/routes/+layout.svelte b/apps/publisher-dashboard/src/routes/+layout.svelte index 80c8b2c..d3b1642 100644 --- a/apps/publisher-dashboard/src/routes/+layout.svelte +++ b/apps/publisher-dashboard/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import "../app.css"; import type { Snippet } from "svelte"; import { QueryClient, QueryClientProvider } from "@tanstack/svelte-query"; import { SvelteQueryDevtools } from "@tanstack/svelte-query-devtools"; +import { Toaster } from "svelte-sonner"; interface Props { children: Snippet; @@ -24,3 +25,4 @@ const queryClient = new QueryClient({ {@render children()} + diff --git a/apps/publisher-dashboard/src/routes/auth/+layout.svelte b/apps/publisher-dashboard/src/routes/auth/+layout.svelte new file mode 100644 index 0000000..3ae9eba --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/+layout.svelte @@ -0,0 +1,89 @@ + + + + Publisher Dashboard + + +
+ + + + +
+
+ +
+
+ + + +
+ Publisher Dashboard +
+ + + {@render children()} + + +

+ By continuing, you agree to our + Terms of Service + and + Privacy Policy +

+
+
+
diff --git a/apps/publisher-dashboard/src/routes/auth/confirm/+page.svelte b/apps/publisher-dashboard/src/routes/auth/confirm/+page.svelte new file mode 100644 index 0000000..00269a9 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/confirm/+page.svelte @@ -0,0 +1,162 @@ + + +
+ +
+
+ +
+
+

Check your email

+

+ We sent a verification link to + {getMaskedEmail()} +

+
+
+ + +
+ + Waiting for email verification... +
+ + + + + + + {#if statusQuery.data?.status === "expired"} + + + + Your verification link has expired. Please request a new one. + + + {/if} + + +
+ {#if isResending} + + Resend email + + {:else} + + {/if} + +
+ +
+
+ + + {#if import.meta.env.DEV} +
+

+ Development Mode +

+

+ To complete login without email, use the CLI command: +

+ + bun run cli auth complete-login --email {loginFlowState.email} + +
+ {/if} +
diff --git a/apps/publisher-dashboard/src/routes/auth/forgot-password/+page.svelte b/apps/publisher-dashboard/src/routes/auth/forgot-password/+page.svelte new file mode 100644 index 0000000..7ad525c --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/forgot-password/+page.svelte @@ -0,0 +1,126 @@ + + + + Forgot Password | Publisher Dashboard + + + + +
+

Forgot password?

+

+ {#if isSubmitted} + Check your email for reset instructions + {:else} + Enter your email and we'll send you reset instructions + {/if} +

+
+ +{#if isSubmitted} + + + + + If an account exists with this email, we've sent password reset instructions. + + + +
+

+ Didn't receive the email? Check your spam folder or try again with a different email. +

+ + +
+{:else} + +
+
+ + +
+ + + + + Send reset instructions + + +{/if} + + +
+ Remember your password?{" "} + + Sign in + +
diff --git a/apps/publisher-dashboard/src/routes/auth/login/+page.svelte b/apps/publisher-dashboard/src/routes/auth/login/+page.svelte new file mode 100644 index 0000000..2abd865 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/login/+page.svelte @@ -0,0 +1,83 @@ + + +
+ +
+

Welcome back

+

Enter your email to sign in to your account

+
+ + + + + +
+
+ + +
+ + + Continue + +
+ + +
+

+ Don't have an account? + + Sign up + +

+
+
diff --git a/apps/publisher-dashboard/src/routes/auth/login/passkey/+page.svelte b/apps/publisher-dashboard/src/routes/auth/login/passkey/+page.svelte new file mode 100644 index 0000000..ac2f594 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/login/passkey/+page.svelte @@ -0,0 +1,152 @@ + + + + Passkey Authentication | Publisher Dashboard + + + +
+ +
+
+ {#if isAuthenticating} + + {:else if error} + + {:else} + + {/if} +
+ +
+

+ {#if isAuthenticating} + Authenticating... + {:else if error} + Authentication failed + {:else} + Use your passkey + {/if} +

+ +

+ {#if isAuthenticating} + Follow the prompts to authenticate with your passkey + {:else if maskedEmail} + Signing in as {maskedEmail} + {:else} + Use your passkey to sign in + {/if} +

+
+
+ + + {#if isAuthenticating && !error && !hasAttempted} +
+ + Waiting for passkey authentication... +
+ {/if} + + + + + +
+ {#if error || hasAttempted} + + Try again + + {/if} + + + {#if loginFlowState.hasPassword} + + {/if} + +
+ +
+
+
diff --git a/apps/publisher-dashboard/src/routes/auth/login/password/+page.svelte b/apps/publisher-dashboard/src/routes/auth/login/password/+page.svelte new file mode 100644 index 0000000..7202733 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/login/password/+page.svelte @@ -0,0 +1,112 @@ + + +
+ +
+

Enter your password

+

+ Signing in as {loginFlowState.email} +

+
+ + + + + +
+
+ + +
+ + + Sign in + +
+ + +
+ + Forgot password? + + + {#if loginFlowState.hasPasskey} + + {/if} + +
+ +
+
+
diff --git a/apps/publisher-dashboard/src/routes/auth/reset-password/+page.svelte b/apps/publisher-dashboard/src/routes/auth/reset-password/+page.svelte new file mode 100644 index 0000000..31889ec --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/reset-password/+page.svelte @@ -0,0 +1,154 @@ + + + + Reset Password | Publisher Dashboard + + + + +
+

Set new password

+

+ Create a strong password for your account +

+
+ +{#if !token} + + + + + Invalid password reset link. Please request a new one. + + + + +{:else} + +
+
+ + + +
+ +
+ + + {#if confirmPassword && !passwordsMatch} +

Passwords do not match

+ {/if} +
+ + + + + Reset password + + +{/if} + + +
+ Remember your password?{" "} + + Sign in + +
diff --git a/apps/publisher-dashboard/src/routes/auth/setup/user/+page.svelte b/apps/publisher-dashboard/src/routes/auth/setup/user/+page.svelte new file mode 100644 index 0000000..7cfee3b --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/setup/user/+page.svelte @@ -0,0 +1,163 @@ + + + + Complete Your Profile | Publisher Dashboard + + +{#if userQuery.isLoading} +
+ +
+{:else if userQuery.error} + + + Failed to load user data. Please try again. + +{:else} + +
+

Complete your profile

+

+ Tell us a bit about yourself to get started +

+
+ + +
+
+ + +

+ This will be shown to other team members +

+
+ +
+ + +
+ +
+ + + {#if phoneError} +

{phoneError}

+ {:else} +

+ Include country code for international numbers +

+ {/if} +
+ + + + + Continue to Dashboard + + +{/if} diff --git a/apps/publisher-dashboard/src/routes/auth/signup/+page.svelte b/apps/publisher-dashboard/src/routes/auth/signup/+page.svelte new file mode 100644 index 0000000..69397d3 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/signup/+page.svelte @@ -0,0 +1,256 @@ + + + + Create Account | Publisher Dashboard + + + + +
+

Create an account

+

+ {#if authMode === "passkey"} + Use a passkey for secure, passwordless authentication + {:else} + Enter your details to create your account + {/if} +

+
+ + +
+
+ + +
+ + {#if authMode === "password"} +
+ + + +
+ +
+ + + {#if confirmPassword && !passwordsMatch} +

Passwords do not match

+ {/if} +
+ {/if} + + + + {#if authMode === "passkey"} + + Create with passkey + + +
+ +
+ {:else} + + Create account + + + {#if supportsPasskey} +
+ +
+ {/if} + {/if} + + + +
+ Already have an account?{" "} + + Sign in + +
diff --git a/apps/publisher-dashboard/src/routes/auth/trust-device/+page.svelte b/apps/publisher-dashboard/src/routes/auth/trust-device/+page.svelte new file mode 100644 index 0000000..e404f64 --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/trust-device/+page.svelte @@ -0,0 +1,167 @@ + + + + Trust This Device | Publisher Dashboard + + + +
+

Trust this device?

+

+ Trusted devices can sign in without email verification +

+
+ + +
+
+
+ +
+
+

{browserName} on {osName}

+ {#if deviceQuery.isLoading} +

Loading device info...

+ {:else if deviceQuery.data} +
+ + + {#if deviceQuery.data.city && deviceQuery.data.country} + {deviceQuery.data.city}, {deviceQuery.data.country} + {:else if deviceQuery.data.country} + {deviceQuery.data.country} + {:else} + Unknown location + {/if} + +
+

+ IP: {deviceQuery.data.ip} +

+ {/if} +
+
+ + +
+ + +

+ Give this device a name to recognize it later +

+
+
+ + +
+ +
+

Only trust devices you own

+

Don't trust shared or public computers. You can manage trusted devices in your account settings.

+
+
+ + + + +
+ + Yes, trust this device + + + +
diff --git a/apps/publisher-dashboard/src/routes/auth/verify/+page.svelte b/apps/publisher-dashboard/src/routes/auth/verify/+page.svelte new file mode 100644 index 0000000..95bea6d --- /dev/null +++ b/apps/publisher-dashboard/src/routes/auth/verify/+page.svelte @@ -0,0 +1,143 @@ + + + + Verify Email | Publisher Dashboard + + + +
+ +
+
+ {#if isVerifying} + + {:else if error} + + {:else} + + {/if} +
+ +
+

+ {#if isVerifying} + Verifying your email... + {:else if error} + Verification failed + {:else} + Email verified! + {/if} +

+ +

+ {#if isVerifying} + Please wait while we verify your email address + {:else if error} + We could not verify your email address + {:else} + Redirecting to your dashboard... + {/if} +

+
+
+ + + {#if isVerifying} +
+ + Verifying... +
+ {/if} + + + {#if error} + + + +
+ {#if token} + + {/if} + + + + +
+ {/if} +
diff --git a/apps/publisher-dashboard/src/routes/login/+layout.svelte b/apps/publisher-dashboard/src/routes/login/+layout.svelte deleted file mode 100644 index 794ade9..0000000 --- a/apps/publisher-dashboard/src/routes/login/+layout.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{@render children()} diff --git a/apps/publisher-dashboard/src/routes/login/+page.svelte b/apps/publisher-dashboard/src/routes/login/+page.svelte index 238c8a5..bda2dd1 100644 --- a/apps/publisher-dashboard/src/routes/login/+page.svelte +++ b/apps/publisher-dashboard/src/routes/login/+page.svelte @@ -1,211 +1,12 @@ - - Login | Publisher Dashboard - - - -
- - - - -
-
- -
-
- - - -
- Publisher Dashboard -
- - -
-

Welcome back

-

- Enter your credentials to access your dashboard -

-
- - -
-
- - -
- -
-
- - - Forgot password? - -
- -
- - {#if error} -

{error}

- {/if} - - -
- - -
-
- -
-
- Or continue with -
-
- - -
- - -
- - -

- By continuing, you agree to our - Terms of Service - and - Privacy Policy -

-
-
+
+

Redirecting...

diff --git a/bun.lock b/bun.lock index cbc6b1b..33d173c 100644 --- a/bun.lock +++ b/bun.lock @@ -62,13 +62,18 @@ "@orpc/client": "^1.13.2", "@orpc/contract": "^1.13.2", "@reviq/api-contract": "workspace:*", + "@simplewebauthn/browser": "^13.2.2", "@tanstack/svelte-query": "^6.0.14", "@tanstack/svelte-query-devtools": "^6.0.3", "bits-ui": "^2.15.4", "clsx": "^2.1.1", + "libphonenumber-js": "^1.12.33", + "svelte-sonner": "^1.0.7", "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2", "tslib": "catalog:", + "ua-parser-js": "^2.0.7", + "zxcvbn": "^4.4.2", }, "devDependencies": { "@internationalized/date": "^3.10.1", @@ -79,6 +84,8 @@ "@sveltejs/kit": "^2.49.4", "@sveltejs/vite-plugin-svelte": "^6.2.3", "@tailwindcss/vite": "^4.1.4", + "@types/ua-parser-js": "^0.7.39", + "@types/zxcvbn": "^4.4.5", "eslint": "catalog:", "svelte": "^5.28.2", "svelte-check": "^4.2.1", @@ -397,6 +404,8 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="], + "@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="], "@simplewebauthn/types": ["@simplewebauthn/types@12.0.0", "", {}, "sha512-q6y8MkoV8V8jB4zzp18Uyj2I7oFp2/ONL8c3j8uT06AOWu3cIChc1au71QYHrP2b+xDapkGTiv+9lX7xkTlAsA=="], @@ -467,6 +476,8 @@ "@types/pg": ["@types/pg@8.16.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ=="], + "@types/ua-parser-js": ["@types/ua-parser-js@0.7.39", "", {}, "sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg=="], + "@types/zxcvbn": ["@types/zxcvbn@4.4.5", "", {}, "sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], @@ -557,6 +568,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "detect-europe-js": ["detect-europe-js@0.1.2", "", {}, "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], @@ -689,6 +702,8 @@ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + "is-standalone-pwa": ["is-standalone-pwa@0.1.1", "", {}, "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -893,6 +908,8 @@ "svelte-check": ["svelte-check@4.3.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q=="], + "svelte-sonner": ["svelte-sonner@1.0.7", "", { "dependencies": { "runed": "^0.28.0" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-1EUFYmd7q/xfs2qCHwJzGPh9n5VJ3X6QjBN10fof2vxgy8fYE7kVfZ7uGnd7i6fQaWIr5KvXcwYXE/cmTEjk5A=="], + "svelte-toolbelt": ["svelte-toolbelt@0.10.6", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.35.1", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.30.2" } }, "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ=="], "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], @@ -943,6 +960,10 @@ "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="], + "ua-is-frozen": ["ua-is-frozen@0.1.2", "", {}, "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw=="], + + "ua-parser-js": ["ua-parser-js@2.0.7", "", { "dependencies": { "detect-europe-js": "^0.1.2", "is-standalone-pwa": "^0.1.1", "ua-is-frozen": "^0.1.2" }, "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-CFdHVHr+6YfbktNZegH3qbYvYgC7nRNEUm2tk7nSFXSODUu4tDBpaFpP1jdXBUOKKwapVlWRfTtS8bCPzsQ47w=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -997,6 +1018,8 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], diff --git a/docs/initial-app.md b/docs/initial-app.md index e9cfbee..024837f 100644 --- a/docs/initial-app.md +++ b/docs/initial-app.md @@ -1982,8 +1982,8 @@ bun add -D kysely-codegen @types/pg @types/zxcvbn @types/ua-parser-js @types/geo ### Frontend ```bash -bun add @orpc/client @orpc/tanstack-query @simplewebauthn/browser @tanstack/svelte-query libphonenumber-js zxcvbn -bun add -D @types/zxcvbn +bun add @orpc/client @orpc/tanstack-query @simplewebauthn/browser @tanstack/svelte-query libphonenumber-js zxcvbn ua-parser-js svelte-sonner +bun add -D @types/zxcvbn @types/ua-parser-js ``` | Package | Purpose | @@ -1994,6 +1994,8 @@ bun add -D @types/zxcvbn | `@tanstack/svelte-query` | Server state management, caching, refetching | | `libphonenumber-js` | Phone number parsing/validation/formatting | | `zxcvbn` | Client-side password strength estimation | +| `ua-parser-js` | User-Agent parsing for device name display | +| `svelte-sonner` | Toast notifications for success/error states | ### CLI @@ -2275,16 +2277,16 @@ _Can run parallel to D, E, F_ _Depends on: D1-D9, E1-E4, C3_ -- [ ] **H1**: Create `/auth/signup` page (passkey detection, password fallback) -- [ ] **H2**: Create `/auth/setup/user` page (profile setup) -- [ ] **H3**: Create `/auth/login` page (email entry, createLoginRequest) -- [ ] **H4**: Create `/auth/login/passkey` page (WebAuthn flow) -- [ ] **H5**: Create `/auth/login/password` page -- [ ] **H6**: Create `/auth/confirm` page (polling for email confirmation) -- [ ] **H7**: Create `/auth/trust-device` page -- [ ] **H8**: Create `/auth/verify` page (email verification callback) -- [ ] **H9**: Create `/auth/forgot-password` page -- [ ] **H10**: Create `/auth/reset-password` page +- [x] **H1**: Create `/auth/signup` page (passkey detection, password fallback) +- [x] **H2**: Create `/auth/setup/user` page (profile setup) +- [x] **H3**: Create `/auth/login` page (email entry, createLoginRequest) +- [x] **H4**: Create `/auth/login/passkey` page (WebAuthn flow) +- [x] **H5**: Create `/auth/login/password` page +- [x] **H6**: Create `/auth/confirm` page (polling for email confirmation) +- [x] **H7**: Create `/auth/trust-device` page +- [x] **H8**: Create `/auth/verify` page (email verification callback) +- [x] **H9**: Create `/auth/forgot-password` page +- [x] **H10**: Create `/auth/reset-password` page #### Workstream I: Account Pages (Frontend)