Fix floating promise lint errors and apply code formatting

- Add void operator to async calls in $effect() blocks to satisfy
  noFloatingPromises lint rule:
  - passkey/+page.svelte: void authenticate()
  - verify/+page.svelte: void verifyEmail()
- Apply biome formatter import reorganization across auth files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 16:39:33 +08:00
parent 96807bdc3f
commit 82078b3a05
25 changed files with 692 additions and 637 deletions

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { AlertCircle } from "@lucide/svelte";
import { AlertCircle } from "@lucide/svelte"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
interface Props { interface Props {
message: string; message: string;
} }
let { message }: Props = $props(); let { message }: Props = $props();
</script> </script>
{#if message} {#if message}

View File

@@ -1,4 +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 PasswordInput } from "./password-input.svelte";
export { default as PasswordStrength } from "./password-strength.svelte"; export { default as PasswordStrength } from "./password-strength.svelte";
export { default as PasswordFormField } from "./password-form-field.svelte";
export { default as ErrorAlert } from "./error-alert.svelte";

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { Label } from "$lib/components/ui/label"; import type { HTMLInputAttributes } from "svelte/elements";
import PasswordInput from "./password-input.svelte"; import { Label } from "$lib/components/ui/label";
import PasswordStrength from "./password-strength.svelte"; import PasswordInput from "./password-input.svelte";
import type { HTMLInputAttributes } from "svelte/elements"; import PasswordStrength from "./password-strength.svelte";
interface Props { interface Props {
id?: string; id?: string;
label?: string; label?: string;
value?: string; value?: string;
@@ -14,9 +14,9 @@
autocomplete?: HTMLInputAttributes["autocomplete"]; autocomplete?: HTMLInputAttributes["autocomplete"];
required?: boolean; required?: boolean;
disabled?: boolean; disabled?: boolean;
} }
let { let {
id = "password", id = "password",
label = "Password", label = "Password",
value = $bindable(""), value = $bindable(""),
@@ -26,7 +26,7 @@
autocomplete = "current-password", autocomplete = "current-password",
required = false, required = false,
disabled = false, disabled = false,
}: Props = $props(); }: Props = $props();
</script> </script>
<div class="space-y-2"> <div class="space-y-2">

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements"; import type { HTMLInputAttributes } from "svelte/elements";
import { Input } from "$lib/components/ui/input"; import { Eye, EyeOff } from "@lucide/svelte";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Eye, EyeOff } from "@lucide/svelte"; import { Input } from "$lib/components/ui/input";
import { cn } from "$lib/utils"; import { cn } from "$lib/utils";
interface Props { interface Props {
value?: string; value?: string;
placeholder?: string; placeholder?: string;
id?: string; id?: string;
@@ -14,9 +14,9 @@
disabled?: boolean; disabled?: boolean;
autocomplete?: HTMLInputAttributes["autocomplete"]; autocomplete?: HTMLInputAttributes["autocomplete"];
class?: string; class?: string;
} }
let { let {
value = $bindable(""), value = $bindable(""),
placeholder = "Enter your password", placeholder = "Enter your password",
id = "password", id = "password",
@@ -25,13 +25,13 @@
disabled = false, disabled = false,
autocomplete = "current-password", autocomplete = "current-password",
class: className = "", class: className = "",
}: Props = $props(); }: Props = $props();
let showPassword = $state(false); let showPassword = $state(false);
function toggleVisibility() { function toggleVisibility() {
showPassword = !showPassword; showPassword = !showPassword;
} }
</script> </script>
<div class="relative"> <div class="relative">

View File

@@ -1,27 +1,31 @@
<script lang="ts"> <script lang="ts">
import zxcvbn from "zxcvbn"; import zxcvbn from "zxcvbn";
interface Props { interface Props {
password: string; password: string;
userInputs?: string[]; userInputs?: string[];
} }
let { password, userInputs = [] }: Props = $props(); let { password, userInputs = [] }: Props = $props();
// Compute password strength using zxcvbn // Compute password strength using zxcvbn
const result = $derived(password ? zxcvbn(password, userInputs) : null); const result = $derived(password ? zxcvbn(password, userInputs) : null);
const score = $derived(result?.score ?? 0); const score = $derived(result?.score ?? 0);
// Strength labels and colors // Strength labels and colors
const strengthConfig = [ const strengthConfig = [
{ label: "Very weak", color: "bg-destructive", textColor: "text-destructive" }, {
label: "Very weak",
color: "bg-destructive",
textColor: "text-destructive",
},
{ label: "Weak", color: "bg-destructive", textColor: "text-destructive" }, { label: "Weak", color: "bg-destructive", textColor: "text-destructive" },
{ label: "Fair", color: "bg-warning", textColor: "text-warning" }, { label: "Fair", color: "bg-warning", textColor: "text-warning" },
{ label: "Good", color: "bg-success", textColor: "text-success" }, { label: "Good", color: "bg-success", textColor: "text-success" },
{ label: "Strong", color: "bg-success", textColor: "text-success" }, { label: "Strong", color: "bg-success", textColor: "text-success" },
] as const; ] as const;
const config = $derived(strengthConfig[score]); const config = $derived(strengthConfig[score]);
</script> </script>
{#if password} {#if password}

View File

@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div

View File

@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js"; import { cn, type WithElementRef } from "$lib/utils.js";
let { let {
ref = $bindable(null), ref = $bindable(null),
class: className, class: className,
children, children,
...restProps ...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props(); }: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div

View File

@@ -1,7 +1,7 @@
<script lang="ts" module> <script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants"; import { tv, type VariantProps } from "tailwind-variants";
export const alertVariants = tv({ export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: { variants: {
variant: { variant: {
@@ -13,9 +13,9 @@
defaultVariants: { defaultVariants: {
variant: "default", variant: "default",
}, },
}); });
export type AlertVariant = VariantProps<typeof alertVariants>["variant"]; export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script> </script>
<script lang="ts"> <script lang="ts">

View File

@@ -1,7 +1,8 @@
import Root from "./alert.svelte"; import Root from "./alert.svelte";
import Description from "./alert-description.svelte"; import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte"; import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export { type AlertVariant, alertVariants } from "./alert.svelte";
export { export {
Root, Root,

View File

@@ -1,6 +1,3 @@
import Root from "./loading-button.svelte"; import Root from "./loading-button.svelte";
export { export { Root, Root as LoadingButton };
Root,
Root as LoadingButton,
};

View File

@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import { Button, type ButtonProps } from "$lib/components/ui/button"; import type { Snippet } from "svelte";
import { Loader2 } from "@lucide/svelte"; import { Loader2 } from "@lucide/svelte";
import type { Snippet } from "svelte"; import { Button, type ButtonProps } from "$lib/components/ui/button";
interface Props extends ButtonProps { interface Props extends ButtonProps {
loading?: boolean; loading?: boolean;
loadingText?: string; loadingText?: string;
children: Snippet; children: Snippet;
} }
let { let {
loading = false, loading = false,
loadingText, loadingText,
children, children,
disabled, disabled,
...rest ...rest
}: Props = $props(); }: Props = $props();
</script> </script>
<Button disabled={loading || disabled} {...rest}> <Button disabled={loading || disabled} {...rest}>

View File

@@ -61,13 +61,16 @@ export function hasActiveLoginFlow(): boolean {
* Get masked email for display (e.g., "j***@example.com") * Get masked email for display (e.g., "j***@example.com")
*/ */
export function getMaskedEmail(): string { export function getMaskedEmail(): string {
if (!loginFlowState.email) return ""; if (!loginFlowState.email) {
return "";
}
const [local, domain] = loginFlowState.email.split("@"); const [local, domain] = loginFlowState.email.split("@");
if (!domain) return loginFlowState.email; if (!domain) {
return loginFlowState.email;
}
const maskedLocal = const maskedLocal = local.length > 1 ? `${local[0]}***` : `${local}***`;
local.length > 1 ? local[0] + "***" : local + "***";
return `${maskedLocal}@${domain}`; return `${maskedLocal}@${domain}`;
} }

View File

@@ -1,4 +1,7 @@
import { parsePhoneNumberWithError, isValidPhoneNumber } from "libphonenumber-js"; import {
isValidPhoneNumber,
parsePhoneNumberWithError,
} from "libphonenumber-js";
/** /**
* Validates email format using a simple regex pattern. * Validates email format using a simple regex pattern.
@@ -12,8 +15,13 @@ export function isValidEmail(email: string): boolean {
* Validates and formats phone numbers using libphonenumber-js. * Validates and formats phone numbers using libphonenumber-js.
* Returns validation status and optionally the formatted number. * Returns validation status and optionally the formatted number.
*/ */
export function validatePhone(value: string): { valid: boolean; formatted?: string } { export function validatePhone(value: string): {
if (!value) return { valid: true }; valid: boolean;
formatted?: string;
} {
if (!value) {
return { valid: true };
}
try { try {
const phone = parsePhoneNumberWithError(value); const phone = parsePhoneNumberWithError(value);
if (isValidPhoneNumber(phone.number)) { if (isValidPhoneNumber(phone.number)) {
@@ -30,8 +38,11 @@ export function validatePhone(value: string): { valid: boolean; formatted?: stri
*/ */
export function maskEmail(email: string): string { export function maskEmail(email: string): string {
const [local, domain] = email.split("@"); const [local, domain] = email.split("@");
if (!domain) return email; if (!domain) {
const masked = local.length > 1 return email;
}
const masked =
local.length > 1
? `${local[0]}${"*".repeat(Math.min(local.length - 1, 3))}` ? `${local[0]}${"*".repeat(Math.min(local.length - 1, 3))}`
: local; : local;
return `${masked}@${domain}`; return `${masked}@${domain}`;

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
interface Props { interface Props {
children: Snippet; children: Snippet;
} }
let { children }: Props = $props(); let { children }: Props = $props();
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,58 +1,58 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { AlertCircle, Loader2, Mail, RefreshCw } from "@lucide/svelte";
import { createQuery } from "@tanstack/svelte-query"; import { createQuery } from "@tanstack/svelte-query";
import { api } from "$lib/api/client"; import { goto } from "$app/navigation";
import { import { api } from "$lib/api/client";
loginFlowState, import { ErrorAlert } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Button } from "$lib/components/ui/button";
import { LoadingButton } from "$lib/components/ui/loading-button";
import {
clearLoginFlowState, clearLoginFlowState,
getMaskedEmail, getMaskedEmail,
} from "$lib/stores/auth.svelte"; loginFlowState,
import { ErrorAlert } from "$lib/components/auth"; } from "$lib/stores/auth.svelte";
import { Button } from "$lib/components/ui/button";
import { LoadingButton } from "$lib/components/ui/loading-button";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Loader2, Mail, AlertCircle, RefreshCw } from "@lucide/svelte";
let resendCooldown = $state(0); let resendCooldown = $state(0);
let isResending = $state(false); let isResending = $state(false);
let resendError = $state<string | null>(null); let resendError = $state<string | null>(null);
// Guard: redirect to /auth/login if no active login flow // Guard: redirect to /auth/login if no active login flow
$effect(() => { $effect(() => {
if (!loginFlowState.email) { if (!loginFlowState.email) {
goto("/auth/login"); goto("/auth/login");
} }
}); });
// Poll for login completion every 3 seconds // Poll for login completion every 3 seconds
// In Svelte 5 with TanStack Query v6, options are passed as a thunk (function) // In Svelte 5 with TanStack Query v6, options are passed as a thunk (function)
const statusQuery = createQuery(() => ({ const statusQuery = createQuery(() => ({
queryKey: ["loginStatus"], queryKey: ["loginStatus"],
queryFn: () => api.auth.loginIfRequestIsCompleted(), queryFn: () => api.auth.loginIfRequestIsCompleted(),
refetchInterval: 3000, refetchInterval: 3000,
enabled: !!loginFlowState.email, enabled: !!loginFlowState.email,
})); }));
// Watch for completed status // Watch for completed status
// In TanStack Query v6 for Svelte 5, query results are reactive objects (not stores) // In TanStack Query v6 for Svelte 5, query results are reactive objects (not stores)
$effect(() => { $effect(() => {
if (statusQuery.data?.status === "completed") { if (statusQuery.data?.status === "completed") {
clearLoginFlowState(); clearLoginFlowState();
goto(statusQuery.data.redirectTo || "/performance"); goto(statusQuery.data.redirectTo || "/performance");
} }
}); });
// Handle cooldown timer // Handle cooldown timer
$effect(() => { $effect(() => {
if (resendCooldown > 0) { if (resendCooldown > 0) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
resendCooldown = resendCooldown - 1; resendCooldown -= 1;
}, 1000); }, 1000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}); });
async function handleResendEmail() { async function handleResendEmail() {
resendError = null; resendError = null;
isResending = true; isResending = true;
@@ -64,12 +64,12 @@
} finally { } finally {
isResending = false; isResending = false;
} }
} }
function handleDifferentEmail() { function handleDifferentEmail() {
clearLoginFlowState(); clearLoginFlowState();
goto("/auth/login"); goto("/auth/login");
} }
</script> </script>
<div class="space-y-6"> <div class="space-y-6">

View File

@@ -1,25 +1,27 @@
<script lang="ts"> <script lang="ts">
import { api } from "$lib/api/client"; import { CheckCircle2 } from "@lucide/svelte";
import { ErrorAlert } from "$lib/components/auth"; import { api } from "$lib/api/client";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Input } from "$lib/components/ui/input"; import { Button } from "$lib/components/ui/button";
import { Label } from "$lib/components/ui/label"; import { Input } from "$lib/components/ui/input";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { Label } from "$lib/components/ui/label";
import { CheckCircle2 } from "@lucide/svelte"; import { LoadingButton } from "$lib/components/ui/loading-button";
// Form state // Form state
let email = $state(""); let email = $state("");
let isLoading = $state(false); let isLoading = $state(false);
let error = $state(""); let error = $state("");
let isSubmitted = $state(false); let isSubmitted = $state(false);
// Form validation // Form validation
const isFormValid = $derived(email.length > 0 && email.includes("@")); const isFormValid = $derived(email.length > 0 && email.includes("@"));
async function handleSubmit(e: Event) { async function handleSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
if (!isFormValid) return; if (!isFormValid) {
return;
}
isLoading = true; isLoading = true;
error = ""; error = "";
@@ -40,7 +42,7 @@
} finally { } finally {
isLoading = false; isLoading = false;
} }
} }
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { setLoginFlowState } from "$lib/stores/auth.svelte"; import { ErrorAlert } from "$lib/components/auth";
import { ErrorAlert } from "$lib/components/auth"; import { Input } from "$lib/components/ui/input";
import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label";
import { Label } from "$lib/components/ui/label"; import { LoadingButton } from "$lib/components/ui/loading-button";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { setLoginFlowState } from "$lib/stores/auth.svelte";
let email = $state(""); let email = $state("");
let isLoading = $state(false); let isLoading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
error = null; error = null;
isLoading = true; isLoading = true;
@@ -32,7 +32,7 @@
error = err instanceof Error ? err.message : "An unexpected error occurred"; error = err instanceof Error ? err.message : "An unexpected error occurred";
isLoading = false; isLoading = false;
} }
} }
</script> </script>
<div class="space-y-6"> <div class="space-y-6">

View File

@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { Fingerprint, KeyRound, Loader2 } from "@lucide/svelte";
import { startAuthentication } from "@simplewebauthn/browser"; import { startAuthentication } from "@simplewebauthn/browser";
import { goto } from "$app/navigation";
import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { LoadingButton } from "$lib/components/ui/loading-button";
import { api } from "$lib/api/client"; import { getMaskedEmail, loginFlowState } from "$lib/stores/auth.svelte";
import { loginFlowState, getMaskedEmail } from "$lib/stores/auth.svelte";
import { Fingerprint, KeyRound, Loader2 } from "@lucide/svelte";
/** /**
* Passkey authentication page * Passkey authentication page
@@ -32,7 +32,9 @@ async function authenticate(): Promise<void> {
const options = await api.auth.webauthn.createAuthenticationOptions(); const options = await api.auth.webauthn.createAuthenticationOptions();
// Trigger browser WebAuthn prompt // Trigger browser WebAuthn prompt
const credential = await startAuthentication({ optionsJSON: options.options }); const credential = await startAuthentication({
optionsJSON: options.options,
});
// Verify with server // Verify with server
await api.auth.webauthn.verifyAuthentication({ await api.auth.webauthn.verifyAuthentication({
@@ -60,7 +62,7 @@ $effect(() => {
// Auto-trigger authentication on mount // Auto-trigger authentication on mount
$effect(() => { $effect(() => {
if (loginFlowState.email && !hasAttempted) { if (loginFlowState.email && !hasAttempted) {
authenticate(); void authenticate();
} }
}); });
</script> </script>

View File

@@ -1,24 +1,24 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { api } from "$lib/api/client"; import { api } from "$lib/api/client";
import { loginFlowState, clearLoginFlowState } from "$lib/stores/auth.svelte"; import { ErrorAlert, PasswordInput } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { Label } from "$lib/components/ui/label"; import { Label } from "$lib/components/ui/label";
import { PasswordInput, ErrorAlert } from "$lib/components/auth"; import { LoadingButton } from "$lib/components/ui/loading-button";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { clearLoginFlowState, loginFlowState } from "$lib/stores/auth.svelte";
let password = $state(""); let password = $state("");
let isLoading = $state(false); let isLoading = $state(false);
let error = $state<string | null>(null); let error = $state<string | null>(null);
// Guard: redirect to /auth/login if no active login flow // Guard: redirect to /auth/login if no active login flow
$effect(() => { $effect(() => {
if (!loginFlowState.email) { if (!loginFlowState.email) {
goto("/auth/login"); goto("/auth/login");
} }
}); });
async function handleSubmit(e: SubmitEvent) { async function handleSubmit(e: SubmitEvent) {
e.preventDefault(); e.preventDefault();
error = null; error = null;
isLoading = true; isLoading = true;
@@ -28,15 +28,18 @@
// On success, redirect to confirm page for email verification // On success, redirect to confirm page for email verification
goto("/auth/confirm"); goto("/auth/confirm");
} catch (err) { } catch (err) {
error = err instanceof Error ? err.message : "Invalid password. Please try again."; error =
err instanceof Error
? err.message
: "Invalid password. Please try again.";
isLoading = false; isLoading = false;
} }
} }
function handleDifferentEmail() { function handleDifferentEmail() {
clearLoginFlowState(); clearLoginFlowState();
goto("/auth/login"); goto("/auth/login");
} }
</script> </script>
<div class="space-y-6"> <div class="space-y-6">

View File

@@ -1,38 +1,47 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { AlertCircle } from "@lucide/svelte";
import { page } from "$app/stores"; import { toast } from "svelte-sonner";
import { api } from "$lib/api/client"; import zxcvbn from "zxcvbn";
import { PasswordInput, PasswordStrength, ErrorAlert } from "$lib/components/auth"; import { goto } from "$app/navigation";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { page } from "$app/stores";
import { Button } from "$lib/components/ui/button"; import { api } from "$lib/api/client";
import { Label } from "$lib/components/ui/label"; import {
import { LoadingButton } from "$lib/components/ui/loading-button"; ErrorAlert,
import { AlertCircle } from "@lucide/svelte"; PasswordInput,
import { toast } from "svelte-sonner"; PasswordStrength,
import zxcvbn from "zxcvbn"; } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { Button } from "$lib/components/ui/button";
import { Label } from "$lib/components/ui/label";
import { LoadingButton } from "$lib/components/ui/loading-button";
// Get token from URL // Get token from URL
const token = $derived($page.url.searchParams.get("token") ?? ""); const token = $derived($page.url.searchParams.get("token") ?? "");
// Form state // Form state
let newPassword = $state(""); let newPassword = $state("");
let confirmPassword = $state(""); let confirmPassword = $state("");
let isLoading = $state(false); let isLoading = $state(false);
let error = $state(""); let error = $state("");
// Password validation // Password validation
const passwordScore = $derived(newPassword ? zxcvbn(newPassword).score : 0); const passwordScore = $derived(newPassword ? zxcvbn(newPassword).score : 0);
const passwordsMatch = $derived(newPassword === confirmPassword); const passwordsMatch = $derived(newPassword === confirmPassword);
const isPasswordValid = $derived( const isPasswordValid = $derived(
newPassword.length >= 8 && passwordScore >= 3 && passwordsMatch && confirmPassword.length > 0 newPassword.length >= 8 &&
); passwordScore >= 3 &&
passwordsMatch &&
confirmPassword.length > 0,
);
// Form validation // Form validation
const isFormValid = $derived(token.length > 0 && isPasswordValid); const isFormValid = $derived(token.length > 0 && isPasswordValid);
async function handleSubmit(e: Event) { async function handleSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
if (!isFormValid) return; if (!isFormValid) {
return;
}
isLoading = true; isLoading = true;
error = ""; error = "";
@@ -52,7 +61,8 @@
if (err instanceof Error) { if (err instanceof Error) {
// Handle specific error cases // Handle specific error cases
if (err.message.includes("expired") || err.message.includes("invalid")) { if (err.message.includes("expired") || err.message.includes("invalid")) {
error = "This password reset link has expired or is invalid. Please request a new one."; error =
"This password reset link has expired or is invalid. Please request a new one.";
} else { } else {
error = err.message; error = err.message;
} }
@@ -62,7 +72,7 @@
} finally { } finally {
isLoading = false; isLoading = false;
} }
} }
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,39 +1,39 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { AlertCircle, Loader2 } from "@lucide/svelte";
import { api } from "$lib/api/client"; import { createQuery } from "@tanstack/svelte-query";
import { ErrorAlert } from "$lib/components/auth"; import { toast } from "svelte-sonner";
import { Button } from "$lib/components/ui/button"; import { goto } from "$app/navigation";
import { Input } from "$lib/components/ui/input"; import { api } from "$lib/api/client";
import { Label } from "$lib/components/ui/label"; import { ErrorAlert } from "$lib/components/auth";
import { Alert, AlertDescription } from "$lib/components/ui/alert"; import { Alert, AlertDescription } from "$lib/components/ui/alert";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { Button } from "$lib/components/ui/button";
import { AlertCircle, Loader2 } from "@lucide/svelte"; import { Input } from "$lib/components/ui/input";
import { createQuery } from "@tanstack/svelte-query"; import { Label } from "$lib/components/ui/label";
import { toast } from "svelte-sonner"; import { LoadingButton } from "$lib/components/ui/loading-button";
import { validatePhone } from "$lib/utils/validation"; import { validatePhone } from "$lib/utils/validation";
// Fetch current user to check if setup is needed // Fetch current user to check if setup is needed
// TanStack Query v6 with Svelte 5: options passed as thunk, results accessed directly // TanStack Query v6 with Svelte 5: options passed as thunk, results accessed directly
const userQuery = createQuery(() => ({ const userQuery = createQuery(() => ({
queryKey: ["me"], queryKey: ["me"],
queryFn: () => api.me.get(), queryFn: () => api.me.get(),
})); }));
// Redirect if user doesn't need setup // Redirect if user doesn't need setup
$effect(() => { $effect(() => {
if (userQuery.data && !userQuery.data.needsSetup) { if (userQuery.data && !userQuery.data.needsSetup) {
goto("/performance"); goto("/performance");
} }
}); });
let displayName = $state(""); let displayName = $state("");
let fullName = $state(""); let fullName = $state("");
let phoneNumber = $state(""); let phoneNumber = $state("");
let isSubmitting = $state(false); let isSubmitting = $state(false);
let error = $state(""); let error = $state("");
let phoneError = $state(""); let phoneError = $state("");
function handlePhoneBlur() { function handlePhoneBlur() {
const result = validatePhone(phoneNumber); const result = validatePhone(phoneNumber);
if (phoneNumber && !result.valid) { if (phoneNumber && !result.valid) {
phoneError = "Please enter a valid phone number (e.g., +1 555 123 4567)"; phoneError = "Please enter a valid phone number (e.g., +1 555 123 4567)";
@@ -43,17 +43,19 @@
phoneNumber = result.formatted; phoneNumber = result.formatted;
} }
} }
} }
const isValid = $derived( const isValid = $derived(
displayName.trim().length >= 1 && displayName.trim().length >= 1 &&
displayName.trim().length <= 100 && displayName.trim().length <= 100 &&
(!phoneNumber || validatePhone(phoneNumber).valid) (!phoneNumber || validatePhone(phoneNumber).valid),
); );
async function handleSubmit(e: Event) { async function handleSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
if (!isValid || isSubmitting) return; if (!isValid || isSubmitting) {
return;
}
isSubmitting = true; isSubmitting = true;
error = ""; error = "";
@@ -72,7 +74,7 @@
} finally { } finally {
isSubmitting = false; isSubmitting = false;
} }
} }
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,56 +1,69 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import {
import { api } from "$lib/api/client"; browserSupportsWebAuthn,
import { PasswordInput, PasswordStrength, ErrorAlert } from "$lib/components/auth"; startRegistration,
import { Input } from "$lib/components/ui/input"; } from "@simplewebauthn/browser";
import { Label } from "$lib/components/ui/label"; import zxcvbn from "zxcvbn";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { goto } from "$app/navigation";
import { browserSupportsWebAuthn, startRegistration } from "@simplewebauthn/browser"; import { api } from "$lib/api/client";
import zxcvbn from "zxcvbn"; import {
ErrorAlert,
PasswordInput,
PasswordStrength,
} from "$lib/components/auth";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { LoadingButton } from "$lib/components/ui/loading-button";
// Form state // Form state
let email = $state(""); let email = $state("");
let password = $state(""); let password = $state("");
let confirmPassword = $state(""); let confirmPassword = $state("");
let isLoading = $state(false); let isLoading = $state(false);
let error = $state(""); let error = $state("");
// Authentication mode: "passkey" or "password" // Authentication mode: "passkey" or "password"
let authMode = $state<"passkey" | "password">("passkey"); let authMode = $state<"passkey" | "password">("passkey");
// Check passkey support on mount // Check passkey support on mount
let supportsPasskey = $state(false); let supportsPasskey = $state(false);
$effect(() => { $effect(() => {
supportsPasskey = browserSupportsWebAuthn(); supportsPasskey = browserSupportsWebAuthn();
if (!supportsPasskey) { if (!supportsPasskey) {
authMode = "password"; authMode = "password";
} }
}); });
// Password validation // Password validation
const passwordScore = $derived(password ? zxcvbn(password, [email]).score : 0); const passwordScore = $derived(password ? zxcvbn(password, [email]).score : 0);
const passwordsMatch = $derived(password === confirmPassword); const passwordsMatch = $derived(password === confirmPassword);
const isPasswordValid = $derived( const isPasswordValid = $derived(
password.length >= 8 && passwordScore >= 3 && passwordsMatch && confirmPassword.length > 0 password.length >= 8 &&
); passwordScore >= 3 &&
passwordsMatch &&
confirmPassword.length > 0,
);
// Form validation // Form validation
const isFormValid = $derived( const isFormValid = $derived(
authMode === "passkey" authMode === "passkey"
? email.length > 0 && email.includes("@") ? email.length > 0 && email.includes("@")
: email.length > 0 && email.includes("@") && isPasswordValid : email.length > 0 && email.includes("@") && isPasswordValid,
); );
async function handlePasskeySignup() { async function handlePasskeySignup() {
isLoading = true; isLoading = true;
error = ""; error = "";
try { try {
// Step 1: Get registration options from server // Step 1: Get registration options from server
const { challengeId, options } = await api.auth.webauthn.createRegistrationOptions({ email }); const { challengeId, options } =
await api.auth.webauthn.createRegistrationOptions({ email });
// Step 2: Start WebAuthn registration // Step 2: Start WebAuthn registration
const registrationResponse = await startRegistration({ optionsJSON: options }); const registrationResponse = await startRegistration({
optionsJSON: options,
});
// Step 3: Complete signup with passkey info // Step 3: Complete signup with passkey info
await api.auth.signup({ await api.auth.signup({
@@ -77,9 +90,9 @@
} finally { } finally {
isLoading = false; isLoading = false;
} }
} }
async function handlePasswordSignup() { async function handlePasswordSignup() {
isLoading = true; isLoading = true;
error = ""; error = "";
@@ -100,28 +113,30 @@
} finally { } finally {
isLoading = false; isLoading = false;
} }
} }
async function handleSubmit(e: Event) { async function handleSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
if (!isFormValid) return; if (!isFormValid) {
return;
}
if (authMode === "passkey") { if (authMode === "passkey") {
await handlePasskeySignup(); await handlePasskeySignup();
} else { } else {
await handlePasswordSignup(); await handlePasswordSignup();
} }
} }
function switchToPassword() { function switchToPassword() {
authMode = "password"; authMode = "password";
} }
function switchToPasskey() { function switchToPasskey() {
authMode = "passkey"; authMode = "passkey";
password = ""; password = "";
confirmPassword = ""; confirmPassword = "";
} }
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,46 +1,50 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { MapPin, Monitor, Shield, Smartphone, Tablet } from "@lucide/svelte";
import { api } from "$lib/api/client"; import { createQuery } from "@tanstack/svelte-query";
import { ErrorAlert } from "$lib/components/auth"; import { toast } from "svelte-sonner";
import { Button } from "$lib/components/ui/button"; import { UAParser } from "ua-parser-js";
import { Input } from "$lib/components/ui/input"; import { goto } from "$app/navigation";
import { Label } from "$lib/components/ui/label"; import { api } from "$lib/api/client";
import { LoadingButton } from "$lib/components/ui/loading-button"; import { ErrorAlert } from "$lib/components/auth";
import { Monitor, Smartphone, Tablet, Shield, MapPin } from "@lucide/svelte"; import { Button } from "$lib/components/ui/button";
import { createQuery } from "@tanstack/svelte-query"; import { Input } from "$lib/components/ui/input";
import { toast } from "svelte-sonner"; import { Label } from "$lib/components/ui/label";
import { UAParser } from "ua-parser-js"; import { LoadingButton } from "$lib/components/ui/loading-button";
// Fetch device info from server // Fetch device info from server
// TanStack Query v6 with Svelte 5: options passed as thunk, results accessed directly // TanStack Query v6 with Svelte 5: options passed as thunk, results accessed directly
const deviceQuery = createQuery(() => ({ const deviceQuery = createQuery(() => ({
queryKey: ["deviceInfo"], queryKey: ["deviceInfo"],
queryFn: () => api.me.getDeviceInfo(), queryFn: () => api.me.getDeviceInfo(),
})); }));
// Parse user agent for suggested device name // Parse user agent for suggested device name
const parser = new UAParser(navigator.userAgent); const parser = new UAParser(navigator.userAgent);
const browserName = parser.getBrowser().name || "Browser"; const browserName = parser.getBrowser().name || "Browser";
const osName = parser.getOS().name || "Device"; const osName = parser.getOS().name || "Device";
const deviceType = parser.getDevice().type; const deviceType = parser.getDevice().type;
const suggestedName = $derived(`${browserName} on ${osName}`); const suggestedName = $derived(`${browserName} on ${osName}`);
let deviceName = $state(""); let deviceName = $state("");
let isSubmitting = $state(false); let isSubmitting = $state(false);
let error = $state(""); let error = $state("");
// Initialize device name with suggestion // Initialize device name with suggestion
$effect(() => { $effect(() => {
if (!deviceName && suggestedName) { if (!deviceName && suggestedName) {
deviceName = suggestedName; deviceName = suggestedName;
} }
}); });
const isValid = $derived(deviceName.trim().length >= 1 && deviceName.trim().length <= 100); const isValid = $derived(
deviceName.trim().length >= 1 && deviceName.trim().length <= 100,
);
async function handleTrust() { async function handleTrust() {
if (!isValid || isSubmitting) return; if (!isValid || isSubmitting) {
return;
}
isSubmitting = true; isSubmitting = true;
error = ""; error = "";
@@ -54,14 +58,14 @@
} finally { } finally {
isSubmitting = false; isSubmitting = false;
} }
} }
async function handleSkip() { async function handleSkip() {
goto("/performance"); goto("/performance");
} }
// Get device icon based on type // Get device icon based on type
function getDeviceIcon() { function getDeviceIcon() {
switch (deviceType) { switch (deviceType) {
case "mobile": case "mobile":
return Smartphone; return Smartphone;
@@ -70,9 +74,9 @@
default: default:
return Monitor; return Monitor;
} }
} }
const DeviceIcon = getDeviceIcon(); const DeviceIcon = getDeviceIcon();
</script> </script>
<svelte:head> <svelte:head>

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { page } from "$app/state"; import { CheckCircle2, Loader2, Mail, XCircle } from "@lucide/svelte";
import { goto } from "$app/navigation";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { api } from "$lib/api/client";
import { ErrorAlert } from "$lib/components/auth"; import { ErrorAlert } from "$lib/components/auth";
import { Button } from "$lib/components/ui/button"; import { Button } from "$lib/components/ui/button";
import { api } from "$lib/api/client";
import { Mail, XCircle, CheckCircle2, Loader2 } from "@lucide/svelte";
/** /**
* Email verification callback page * Email verification callback page
@@ -42,7 +42,7 @@ async function verifyEmail(): Promise<void> {
// Auto-verify on mount // Auto-verify on mount
$effect(() => { $effect(() => {
if (token) { if (token) {
verifyEmail(); void verifyEmail();
} else { } else {
error = "No verification token provided"; error = "No verification token provided";
isVerifying = false; isVerifying = false;
@@ -58,7 +58,8 @@ async function resendVerification(): Promise<void> {
toast.success("Verification email sent! Check your inbox."); toast.success("Verification email sent! Check your inbox.");
error = ""; error = "";
} catch (e) { } catch (e) {
const message = e instanceof Error ? e.message : "Failed to send verification email"; const message =
e instanceof Error ? e.message : "Failed to send verification email";
toast.error(message); toast.error(message);
} }
} }

View File

@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
// Redirect old /login route to new /auth/login // Redirect old /login route to new /auth/login
$effect(() => { $effect(() => {
goto("/auth/login", { replaceState: true }); goto("/auth/login", { replaceState: true });
}); });
</script> </script>
<div class="flex min-h-screen items-center justify-center"> <div class="flex min-h-screen items-center justify-center">