release: WRNexusJS 0.5.10
This commit is contained in:
@@ -36,13 +36,9 @@ component AuthenticatorSetup {
|
||||
novalidate
|
||||
class='mt-5 space-y-4 {credentialId == "" ? "" : "hidden"}'
|
||||
>
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-authenticator-label" class="block text-sm font-medium">Authenticator name</label>
|
||||
<input id="auth-authenticator-label" name="label" value="Authenticator" autocomplete="off" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" />
|
||||
<p data-error="label" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input id="auth-authenticator-label" name="label" label="Authenticator name" value="Authenticator" autocomplete="off" icon="icon-[lucide--smartphone]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{setupLabel}</button>
|
||||
<Button type="submit" label="{setupLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
|
||||
<div data-auth-authenticator-details class='{credentialId == "" ? "hidden" : ""}'>
|
||||
@@ -53,15 +49,11 @@ component AuthenticatorSetup {
|
||||
</div>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-auth-authenticator-confirm novalidate class="mt-5 space-y-4">
|
||||
<input type="hidden" name="credentialId" value='{credentialId}' />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-authenticator-code" class="block text-sm font-medium">Authenticator code</label>
|
||||
<input id="auth-authenticator-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
|
||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<PinInput label="Authenticator code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="credentialId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,7 @@ component ForgotPassword {
|
||||
title = "Recover your account"
|
||||
description = "Enter your email, phone, or username."
|
||||
identifierLabel = "Account identifier"
|
||||
identifierPlaceholder = "Enter your registered email, mobile number, or username"
|
||||
submitLabel = "Send recovery link"
|
||||
successMessage = "If an account matches, a recovery link has been sent."
|
||||
signInHref = "/sign-in"
|
||||
@@ -17,14 +18,11 @@ component ForgotPassword {
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-recovery-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
||||
<input id="auth-recovery-identifier" name="identifier" autocomplete="username" aria-describedby="auth-recovery-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p id="auth-recovery-identifier-error" data-error="identifier" role="alert" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input id="auth-recovery-identifier" name="identifier" label="{identifierLabel}" autocomplete="username" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||
<slot></slot>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Back to sign in</a>
|
||||
</section>
|
||||
|
||||
@@ -22,12 +22,12 @@ component InvitationAccept {
|
||||
<div class="mt-5 rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] p-4 text-sm"><p class="m-0 font-semibold">{organization}</p><p class="mb-0 mt-1 text-[var(--wire-color-muted)]">Invited by {inviter}</p></div>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-5 space-y-4">
|
||||
<input type="hidden" name="token" value='{token}' />
|
||||
<div class="space-y-1.5"><label for="auth-invitation-name" class="block text-sm font-medium">Display name</label><input id="auth-invitation-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
||||
<div class="space-y-1.5"><label for="auth-invitation-password" class="block text-sm font-medium">Create password</label><input id="auth-invitation-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
||||
<Input id="auth-invitation-name" name="displayName" label="Display name" autocomplete="name" placeholder="Enter your display name" icon="icon-[lucide--user]" color="{color}" size="{size}" />
|
||||
<StrongPassword label="Create password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ component MagicLinkSignIn {
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mb-0 mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<div class="space-y-1.5"><label for="auth-magic-link-identifier" class="block text-sm font-medium">{emailLabel}</label><input id="auth-magic-link-identifier" name="identifier" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" /><p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
||||
<Input id="auth-magic-link-identifier" name="identifier" type="email" label="{emailLabel}" autocomplete="email" placeholder="Enter your email address" icon="icon-[lucide--mail]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{signInHref}' class="mt-4 inline-flex text-sm font-medium text-[var(--wire-color-primary)] hover:underline">Return to password sign-in</a>
|
||||
</section>
|
||||
|
||||
@@ -41,15 +41,11 @@ component OtpSignIn {
|
||||
class='mt-6 space-y-4 {challengeId == "" ? "" : "hidden"}'
|
||||
>
|
||||
<input type="hidden" name="method" value='{method}' />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-otp-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
||||
<input id="auth-otp-identifier" name="identifier" type="text" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input id="auth-otp-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username" placeholder="Enter your email address or phone number" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{requestLabel}</button>
|
||||
<Button type="submit" label="{requestLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
|
||||
<form
|
||||
@@ -62,15 +58,11 @@ component OtpSignIn {
|
||||
>
|
||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-otp-code" class="block text-sm font-medium">One-time code</label>
|
||||
<input id="auth-otp-code" name="code" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em] outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<PinInput label="One-time code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white disabled:cursor-wait disabled:opacity-60">{verifyLabel}</button>
|
||||
<button type="button" data-auth-otp-back class="w-full text-sm text-[var(--wire-color-primary)] hover:underline">Use a different account</button>
|
||||
<Button type="submit" label="{verifyLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
<Button type="button" data-auth-otp-back label="Use a different account" variant="ghost" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -18,16 +18,11 @@ component ResetPassword {
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="mt-6 space-y-4">
|
||||
<input type="hidden" name="token" value='{token}' />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-reset-password" class="block text-sm font-medium">New password</label>
|
||||
<input id="auth-reset-password" name="password" type="password" autocomplete="new-password" aria-describedby="auth-reset-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
|
||||
<p id="auth-reset-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<StrongPassword label="New password" name="password" autocomplete="new-password" minLength="12" requireLowercase="true" requireUppercase="true" requireNumber="true" requireSpecialCharacter="true" color="{color}" size="{size}" />
|
||||
<p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ component SignIn {
|
||||
title = "Welcome back"
|
||||
description = "Sign in to continue to your account."
|
||||
identifierLabel = "Email, phone, or username"
|
||||
identifierPlaceholder = "Enter your email, mobile number, or username"
|
||||
passwordLabel = "Password"
|
||||
passwordPlaceholder = "Enter your password"
|
||||
submitLabel = "Sign in"
|
||||
forgotHref = "/recover"
|
||||
signUpHref = "/sign-up"
|
||||
@@ -29,22 +31,25 @@ component SignIn {
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
<input type="hidden" name="deviceFingerprint" value="" />
|
||||
<input type="hidden" name="deviceName" value="" />
|
||||
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-sign-in-identifier" class="block text-sm font-medium">{identifierLabel}</label>
|
||||
<input id="auth-sign-in-identifier" name="identifier" type="text" autocomplete="username webauthn" aria-describedby="auth-sign-in-identifier-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
|
||||
<p id="auth-sign-in-identifier-error" data-error="identifier" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between gap-3 text-sm font-medium"><label for="auth-sign-in-password">{passwordLabel}</label><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
|
||||
<input id="auth-sign-in-password" name="password" type="password" autocomplete="current-password" aria-describedby="auth-sign-in-password-error" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-sm outline-none focus:border-[var(--wire-color-primary)] focus:ring-2 focus:ring-[color-mix(in_srgb,var(--wire-color-primary)_25%,transparent)]" />
|
||||
<p id="auth-sign-in-password-error" data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<div class="flex justify-end"><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
|
||||
<TogglePassword
|
||||
label="{passwordLabel}"
|
||||
name="password"
|
||||
placeholder="{passwordPlaceholder}"
|
||||
autocomplete="current-password"
|
||||
pattern=""
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
/>
|
||||
</div>
|
||||
{#if showRemember}
|
||||
<label class="flex items-center gap-2 text-sm text-[var(--wire-color-muted)]"><input name="rememberDevice" type="checkbox" class="size-4 rounded border-[var(--wire-color-border)]" />Trust this device</label>
|
||||
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
|
||||
{/if}
|
||||
<slot></slot>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="inline-flex h-11 w-full items-center justify-center rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
{#if showPasskey}
|
||||
<div class="my-4 flex items-center gap-3 text-xs text-[var(--wire-color-muted)]"><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span><span>or</span><span class="h-px flex-1 bg-[var(--wire-color-border)]"></span></div>
|
||||
|
||||
@@ -7,6 +7,13 @@ component SignUp {
|
||||
description = "Use a strong password and verify your contact details."
|
||||
submitLabel = "Create account"
|
||||
signInHref = "/sign-in"
|
||||
termsHref = "/legal/terms"
|
||||
privacyHref = "/legal/privacy-policy"
|
||||
namePlaceholder = "Enter your full name"
|
||||
emailPlaceholder = "Enter your email address"
|
||||
phonePlaceholder = "Enter your mobile number"
|
||||
usernamePlaceholder = "Choose a username"
|
||||
passwordPlaceholder = "Create a strong password"
|
||||
showPhone = true
|
||||
showUsername = true
|
||||
requireConsent = true
|
||||
@@ -15,10 +22,24 @@ component SignUp {
|
||||
class = ""
|
||||
}
|
||||
view {
|
||||
<section {...attrs} data-wrnexus-runtime="auth" class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'>
|
||||
<header class="mb-6 space-y-1.5">
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="m-0 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<section
|
||||
{...attrs}
|
||||
data-wrnexus-runtime="auth"
|
||||
class='w-full max-w-lg rounded-[var(--wire-radius-lg)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface)] p-6 text-[var(--wire-color-text)] shadow-[var(--wire-shadow-2)] {class}'
|
||||
>
|
||||
<header
|
||||
class="mb-6 space-y-1.5"
|
||||
>
|
||||
<h1
|
||||
class="m-0 text-2xl font-semibold"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p
|
||||
class="m-0 text-sm text-[var(--wire-color-muted)]"
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@@ -29,62 +50,146 @@ component SignUp {
|
||||
novalidate
|
||||
class="grid gap-4 sm:grid-cols-2"
|
||||
>
|
||||
<div class="space-y-1.5 sm:col-span-2">
|
||||
<label for="auth-sign-up-name" class="text-sm font-medium">Full name</label>
|
||||
<input id="auth-sign-up-name" name="displayName" autocomplete="name" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="displayName" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input
|
||||
id="auth-sign-up-name"
|
||||
name="displayName"
|
||||
label="Full name"
|
||||
autocomplete="name"
|
||||
placeholder="{namePlaceholder}"
|
||||
icon="icon-[lucide--user]"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
class="sm:col-span-2"
|
||||
/>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-sign-up-email" class="text-sm font-medium">Email</label>
|
||||
<input id="auth-sign-up-email" name="email" type="email" autocomplete="email" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="email" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input
|
||||
id="auth-sign-up-email"
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
autocomplete="email"
|
||||
placeholder="{emailPlaceholder}"
|
||||
icon="icon-[lucide--mail]"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
/>
|
||||
|
||||
{#if showPhone}
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-sign-up-phone" class="text-sm font-medium">Phone</label>
|
||||
<input id="auth-sign-up-phone" name="phone" type="tel" autocomplete="tel" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="phone" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input
|
||||
id="auth-sign-up-phone"
|
||||
name="phone"
|
||||
type="tel"
|
||||
label="Phone"
|
||||
autocomplete="tel"
|
||||
placeholder="{phonePlaceholder}"
|
||||
icon="icon-[lucide--phone]"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if showUsername}
|
||||
<div class="space-y-1.5 sm:col-span-2">
|
||||
<label for="auth-sign-up-username" class="text-sm font-medium">Username</label>
|
||||
<input id="auth-sign-up-username" name="username" autocomplete="username" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p data-error="username" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Input
|
||||
id="auth-sign-up-username"
|
||||
name="username"
|
||||
label="Username"
|
||||
autocomplete="username"
|
||||
placeholder="{usernamePlaceholder}"
|
||||
icon="icon-[lucide--circle-user-round]"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
class="sm:col-span-2"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-1.5 sm:col-span-2">
|
||||
<label for="auth-sign-up-password" class="text-sm font-medium">Password</label>
|
||||
<input id="auth-sign-up-password" name="password" type="password" autocomplete="new-password" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 outline-none focus:border-[var(--wire-color-primary)]" />
|
||||
<p class="m-0 text-xs text-[var(--wire-color-muted)]">At least 12 characters with uppercase, lowercase, and a number.</p>
|
||||
<p data-error="password" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<div
|
||||
class="space-y-1.5 sm:col-span-2"
|
||||
>
|
||||
<StrongPassword
|
||||
label="Password"
|
||||
name="password"
|
||||
placeholder="{passwordPlaceholder}"
|
||||
autocomplete="new-password"
|
||||
minLength="12"
|
||||
requireLowercase="true"
|
||||
requireUppercase="true"
|
||||
requireNumber="true"
|
||||
requireSpecialCharacter="true"
|
||||
presentation="inline"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if requireConsent}
|
||||
<div class="space-y-1 sm:col-span-2">
|
||||
<label class="flex items-start gap-2 text-sm text-[var(--wire-color-muted)]">
|
||||
<input name="consent" type="checkbox" class="mt-0.5 size-4" />
|
||||
<span>I agree to the terms and privacy policy.</span>
|
||||
</label>
|
||||
<p data-error="consent" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Checkbox
|
||||
id="auth-sign-up-consent"
|
||||
name="consent"
|
||||
label="I agree to the"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
class="sm:col-span-2"
|
||||
>
|
||||
<span
|
||||
class="flex items-center"
|
||||
>
|
||||
<a
|
||||
href="{termsHref}"
|
||||
>
|
||||
terms
|
||||
</a>
|
||||
and
|
||||
<a
|
||||
href="{privacyHref}"
|
||||
>
|
||||
privacy policy
|
||||
</a>.
|
||||
</span>
|
||||
</Checkbox>
|
||||
{:else}
|
||||
<input type="hidden" name="consent" value="true" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="consent"
|
||||
value="true"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="sm:col-span-2"><slot></slot></div>
|
||||
<div
|
||||
class="sm:col-span-2"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"></p>
|
||||
<p data-success="Account created successfully." role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"></p>
|
||||
<p
|
||||
data-error="_form"
|
||||
role="alert"
|
||||
class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)] sm:col-span-2"
|
||||
>
|
||||
</p>
|
||||
<p
|
||||
data-success="Account created successfully."
|
||||
role="status"
|
||||
hidden
|
||||
class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)] sm:col-span-2"
|
||||
>
|
||||
</p>
|
||||
|
||||
<button type="submit" class="h-11 rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] px-4 text-sm font-semibold text-white sm:col-span-2 disabled:cursor-not-allowed disabled:opacity-60">{submitLabel}</button>
|
||||
<Button
|
||||
type="submit"
|
||||
label="{submitLabel}"
|
||||
variant="solid"
|
||||
color="{color}"
|
||||
size="{size}"
|
||||
fullWidth="true"
|
||||
class="sm:col-span-2"
|
||||
/>
|
||||
</form>
|
||||
|
||||
<p class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]">Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a></p>
|
||||
<p
|
||||
class="mb-0 mt-5 text-center text-sm text-[var(--wire-color-muted)]"
|
||||
>
|
||||
Already have an account? <a href='{signInHref}' class="font-semibold text-[var(--wire-color-primary)] hover:underline">Sign in</a>
|
||||
</p>
|
||||
</section>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@ component TwoFactorChallenge {
|
||||
size = "md"
|
||||
class = ""
|
||||
}
|
||||
state methodOptions = [
|
||||
{"label":"Authenticator app","value":"totp"},
|
||||
{"label":"Recovery code","value":"recovery-code"},
|
||||
{"label":"Email code","value":"email-otp"},
|
||||
{"label":"SMS code","value":"sms-otp"}
|
||||
]
|
||||
view {
|
||||
<section
|
||||
{...attrs}
|
||||
@@ -31,26 +37,13 @@ component TwoFactorChallenge {
|
||||
<input type="hidden" name="mfaToken" value='{mfaToken}' />
|
||||
<input type="hidden" name="challengeId" value='{challengeId}' />
|
||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-mfa-method" class="block text-sm font-medium">Verification method</label>
|
||||
<select id="auth-mfa-method" name="method" data-auth-mfa-method class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3">
|
||||
<option value="totp">Authenticator app</option>
|
||||
<option value="recovery-code">Recovery code</option>
|
||||
<option value="email-otp">Email code</option>
|
||||
<option value="sms-otp">SMS code</option>
|
||||
</select>
|
||||
<p data-error="method" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label for="auth-mfa-code" class="block text-sm font-medium">Verification code</label>
|
||||
<input id="auth-mfa-code" name="code" inputmode="numeric" autocomplete="one-time-code" class="h-12 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3 text-center text-xl tracking-[0.35em]" />
|
||||
<p data-error="code" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
</div>
|
||||
<Select id="auth-mfa-method" name="method" data-auth-mfa-method label="Verification method" value="{method}" options="{methodOptions}" color="{color}" size="{size}" />
|
||||
<PinInput label="Verification code" name="code" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-auth-mfa-status role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-primary)_10%,transparent)] p-3 text-sm text-[var(--wire-color-primary)]"></p>
|
||||
<p data-error="mfaToken" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="challengeId" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<a href='{recoveryHref}' class="mt-4 inline-flex text-sm text-[var(--wire-color-primary)] hover:underline">Account recovery options</a>
|
||||
</section>
|
||||
|
||||
@@ -20,17 +20,17 @@ component VerifyEmail {
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<div class="space-y-1.5"><label for="auth-email-token" class="block text-sm font-medium">Verification token</label><input id="auth-email-token" name="token" value='{token}' autocomplete="one-time-code" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
||||
<Input id="auth-email-token" name="token" label="Verification token" value="{token}" autocomplete="one-time-code" placeholder="Enter your verification token" icon="icon-[lucide--badge-check]" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||
<input type="hidden" name="type" value='{kind}' />
|
||||
<input type="hidden" name="identifier" value='{identifier}' />
|
||||
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
|
||||
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -20,17 +20,17 @@ component VerifyPhone {
|
||||
<h1 class="m-0 text-2xl font-semibold">{title}</h1>
|
||||
<p class="mt-2 text-sm text-[var(--wire-color-muted)]">{description}</p>
|
||||
<form method="post" action='{action}' data-schema='{schema}' novalidate class="mt-6 space-y-4">
|
||||
<div class="space-y-1.5"><label for="auth-phone-token" class="block text-sm font-medium">Verification token</label><input id="auth-phone-token" name="token" value='{token}' autocomplete="one-time-code" inputmode="numeric" class="h-11 w-full rounded-[var(--wire-radius-sm)] border border-[var(--wire-color-border)] bg-[var(--wire-color-surface-2)] px-3" /><p data-error="token" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p></div>
|
||||
<PinInput label="Verification token" name="token" value="{token}" length="6" inputMode="numeric" autocomplete="one-time-code" color="{color}" size="{size}" />
|
||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wire-color-danger)]"></p>
|
||||
<p data-success='{successMessage}' role="status" hidden class="m-0 rounded-[var(--wire-radius-sm)] bg-[color-mix(in_srgb,var(--wire-color-success)_10%,transparent)] p-3 text-sm text-[var(--wire-color-success)]">{successMessage}</p>
|
||||
<button type="submit" class="h-11 w-full rounded-[var(--wire-radius-sm)] bg-[var(--wire-color-primary)] font-semibold text-white disabled:cursor-wait disabled:opacity-60">{submitLabel}</button>
|
||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||
</form>
|
||||
<form method="post" action='{resendAction}' data-schema='{resendSchema}' novalidate class="mt-3">
|
||||
<input type="hidden" name="type" value='{kind}' />
|
||||
<input type="hidden" name="identifier" value='{identifier}' />
|
||||
<p data-error="type" class="m-0 min-h-4 text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<p data-error="_form" role="alert" class="m-0 hidden text-xs text-[var(--wire-color-danger)]"></p>
|
||||
<button type="submit" class="text-sm text-[var(--wire-color-primary)] hover:underline disabled:opacity-60">Send a new code</button>
|
||||
<Button type="submit" label="Send a new code" variant="ghost" color="{color}" size="sm" />
|
||||
</form>
|
||||
</section>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.10",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@@ -36,7 +36,7 @@
|
||||
"scripts": {
|
||||
"test": "bun test",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"check": "bun run typecheck \u0026\u0026 bun run test"
|
||||
"check": "bun run typecheck && bun run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
@@ -47,6 +47,7 @@
|
||||
"@wrnexus/oauth": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
"@wrnexus/queue": "workspace:*",
|
||||
"@wrnexus/ui": "workspace:*",
|
||||
"@wrnexus/validation": "workspace:*",
|
||||
"@wrnexus/db": "workspace:*"
|
||||
},
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
getAuthUser,
|
||||
} from "../middleware.ts";
|
||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "../validation.ts";
|
||||
import type { AuthSignedInHandler, AuthSignedOutHandler } from "../types.ts";
|
||||
import type {
|
||||
AuthSessionVerificationHandler,
|
||||
AuthSignedInHandler,
|
||||
AuthSignedOutHandler,
|
||||
} from "../types.ts";
|
||||
|
||||
function text(value: unknown): string {
|
||||
return typeof value === "string" ? value : value == null ? "" : String(value);
|
||||
@@ -53,6 +57,7 @@ export interface AuthHttpOptions {
|
||||
onSignedIn?: AuthSignedInHandler;
|
||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
||||
onSignedOut?: AuthSignedOutHandler;
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
}
|
||||
|
||||
export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
||||
@@ -83,6 +88,21 @@ export function createAuthHttpHandlers(options: AuthHttpOptions) {
|
||||
}
|
||||
|
||||
return {
|
||||
async verifySession(ctx: Context): Promise<Response> {
|
||||
const user = getAuthUser(ctx);
|
||||
if (options.onSessionVerification) {
|
||||
return options.onSessionVerification(ctx, user);
|
||||
}
|
||||
if (!user) return json({ ok: false, error: "Unauthorized" }, 401);
|
||||
if ((ctx.req.headers.get("accept") ?? "").includes("application/json")) {
|
||||
return json({ ok: true, user });
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: { "cache-control": "no-store" },
|
||||
});
|
||||
},
|
||||
|
||||
async register(ctx: Context): Promise<Response> {
|
||||
const validation = await parseBody(schemas.register, ctx.req);
|
||||
if (!validation.ok) return validation.response;
|
||||
|
||||
@@ -12,9 +12,16 @@ function wantsJson(ctx: Context): boolean {
|
||||
return accept.includes("application/json") && !accept.includes("text/html");
|
||||
}
|
||||
|
||||
export function authSession(engine: AuthEngine): Middleware {
|
||||
export interface AuthSessionOptions {
|
||||
/** Optional shared SSO cookie containing an AuthEngine session id. */
|
||||
cookieName?: string;
|
||||
}
|
||||
|
||||
export function authSession(engine: AuthEngine, options: AuthSessionOptions = {}): Middleware {
|
||||
return async (ctx, next) => {
|
||||
const sessionId = ctx.session.get<string>(AUTH_SESSION_KEY);
|
||||
const sessionId =
|
||||
(options.cookieName ? ctx.cookies.get(options.cookieName) : undefined) ??
|
||||
ctx.session.get<string>(AUTH_SESSION_KEY);
|
||||
if (!sessionId) {
|
||||
ctx.user = null;
|
||||
ctx.locals.authUser = null;
|
||||
|
||||
@@ -4,7 +4,11 @@ import { fileURLToPath } from "node:url";
|
||||
import { definePlugin, type PluginContext } from "@wrnexus/plugin";
|
||||
import type { AuthEngine } from "./engine.ts";
|
||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||
import type { AuthSignedInHandler, AuthSignedOutHandler } from "./types.ts";
|
||||
import type {
|
||||
AuthSessionVerificationHandler,
|
||||
AuthSignedInHandler,
|
||||
AuthSignedOutHandler,
|
||||
} from "./types.ts";
|
||||
import { AUTH_ROUTE_DEFINITIONS, type AuthRouteGroup } from "./routes/definitions.ts";
|
||||
import {
|
||||
clearDefaultAuthEngine,
|
||||
@@ -52,6 +56,10 @@ export interface AuthConfig {
|
||||
onSignedIn?: AuthSignedInHandler;
|
||||
/** @deprecated Prefer createAuthEngine({ onSignedOut }). */
|
||||
onSignedOut?: AuthSignedOutHandler;
|
||||
/** Shared SSO cookie whose value is an AuthEngine session id. */
|
||||
sessionCookieName?: string;
|
||||
/** Customize the package forward-auth verification response. */
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
}
|
||||
|
||||
/** Explicit plugin options remain supported for compatibility. Prefer config.auth. */
|
||||
@@ -88,14 +96,22 @@ interface ResolvedAuthConfig {
|
||||
passkey?: AuthPasskeyHttpOptions;
|
||||
onSignedIn?: AuthSignedInHandler;
|
||||
onSignedOut?: AuthSignedOutHandler;
|
||||
sessionCookieName?: string;
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
}
|
||||
|
||||
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
const moduleRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const packageRoot = dirname(moduleRoot);
|
||||
const compiledPackage = moduleRoot === join(packageRoot, "dist");
|
||||
const clientRuntime = join(packageRoot, "assets", "client", "auth.js");
|
||||
const migrationsFile = join(packageRoot, "migrations", "001_auth.sql");
|
||||
const otpPurposeMigrationFile = join(packageRoot, "migrations", "002_auth_otp_purpose.sql");
|
||||
const apiRoutesDir = join(packageRoot, "src", "routes", "api");
|
||||
const middlewareFile = join(packageRoot, "src", "routes", "middleware.ts");
|
||||
const apiRoutesDir = compiledPackage
|
||||
? join(moduleRoot, "routes", "api")
|
||||
: join(packageRoot, "src", "routes", "api");
|
||||
const middlewareFile = compiledPackage
|
||||
? join(moduleRoot, "routes", "middleware.js")
|
||||
: join(packageRoot, "src", "routes", "middleware.ts");
|
||||
const resolvedConfigKey = "@wrnexus/auth:resolved-config";
|
||||
|
||||
export function authComponentsDir(): string {
|
||||
@@ -104,7 +120,7 @@ export function authComponentsDir(): string {
|
||||
|
||||
function authApiRouteEntry(path: string): string {
|
||||
const routeName = path.replace(/^\/api\/auth\/?/, "").replace(/\//g, "-") || "index";
|
||||
return join(apiRoutesDir, routeName + ".ts");
|
||||
return join(apiRoutesDir, routeName + (compiledPackage ? ".js" : ".ts"));
|
||||
}
|
||||
|
||||
function resolveConfig(
|
||||
@@ -142,6 +158,8 @@ function resolveConfig(
|
||||
passkey: raw.passkey,
|
||||
onSignedIn: raw.onSignedIn ?? raw.engine?.onSignedIn,
|
||||
onSignedOut: raw.onSignedOut ?? raw.engine?.onSignedOut,
|
||||
sessionCookieName: raw.sessionCookieName,
|
||||
onSessionVerification: raw.onSessionVerification,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -377,6 +395,10 @@ export function authPlugin(options: AuthPluginOptions = {}) {
|
||||
onSignedIn: value.onSignedIn,
|
||||
|
||||
onSignedOut: value.onSignedOut,
|
||||
|
||||
sessionCookieName: value.sessionCookieName,
|
||||
|
||||
onSessionVerification: value.onSessionVerification,
|
||||
});
|
||||
|
||||
context.metadata.set("@wrnexus/auth:component-dir", value.componentDir);
|
||||
|
||||
@@ -62,6 +62,7 @@ function handlersFor(ctx: Context): AuthHttpHandlers | undefined {
|
||||
passkey: routeOptions.passkey,
|
||||
onSignedIn: routeOptions.onSignedIn,
|
||||
onSignedOut: routeOptions.onSignedOut,
|
||||
onSessionVerification: routeOptions.onSessionVerification,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import { invokeAuthHandler } from "../api.ts";
|
||||
|
||||
export function GET(ctx: Context): Promise<Response> {
|
||||
return invokeAuthHandler("verifySession", ctx);
|
||||
}
|
||||
@@ -23,6 +23,12 @@ export interface AuthRouteDefinition {
|
||||
|
||||
/** Single source of truth for package-contributed auth endpoints. */
|
||||
export const AUTH_ROUTE_DEFINITIONS = [
|
||||
{
|
||||
path: "/api/auth/session/verify",
|
||||
group: "sessions",
|
||||
handler: "verifySession",
|
||||
methods: ["GET"],
|
||||
},
|
||||
{ path: "/api/auth/register", group: "registration", handler: "register", methods: ["POST"] },
|
||||
{ path: "/api/auth/login", group: "login", handler: "login", methods: ["POST"] },
|
||||
{ path: "/api/auth/logout", group: "login", handler: "logout", methods: ["POST"] },
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import type { Middleware } from "@wrnexus/core";
|
||||
import { authSession } from "../middleware.ts";
|
||||
import { getDefaultAuthEngine, hasDefaultAuthEngine } from "../runtime.ts";
|
||||
import {
|
||||
getDefaultAuthEngine,
|
||||
getDefaultAuthRouteOptions,
|
||||
hasDefaultAuthEngine,
|
||||
} from "../runtime.ts";
|
||||
|
||||
/** Package middleware: hydrates auth state when a default engine is configured. */
|
||||
const middleware: Middleware = async (ctx, next) => {
|
||||
if (!hasDefaultAuthEngine()) return next();
|
||||
return authSession(getDefaultAuthEngine())(ctx, next);
|
||||
return authSession(getDefaultAuthEngine(), {
|
||||
cookieName: getDefaultAuthRouteOptions().sessionCookieName,
|
||||
})(ctx, next);
|
||||
};
|
||||
|
||||
export default middleware;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Context } from "@wrnexus/core";
|
||||
import type { AuthEngine } from "./engine.ts";
|
||||
import type { AuthPasskeyHttpOptions } from "./http/index.ts";
|
||||
import type { AuthSessionVerificationHandler } from "./types.ts";
|
||||
import { resolveAuthSchemas, type AuthSchemaOverrides, type AuthSchemaSet } from "./validation.ts";
|
||||
|
||||
export interface DefaultAuthRouteOptions {
|
||||
@@ -11,6 +12,8 @@ export interface DefaultAuthRouteOptions {
|
||||
onSignedIn?: (ctx: Context, returnTo?: string) => Response | Promise<Response>;
|
||||
|
||||
onSignedOut?: (ctx: Context) => Response | Promise<Response>;
|
||||
sessionCookieName?: string;
|
||||
onSessionVerification?: AuthSessionVerificationHandler;
|
||||
}
|
||||
|
||||
interface AuthRuntimeState {
|
||||
|
||||
@@ -241,6 +241,12 @@ export type AuthSignedInHandler = (ctx: Context, returnTo?: string) => Response
|
||||
/** HTTP response hook invoked after the auth engine clears a signed-in session. */
|
||||
export type AuthSignedOutHandler = (ctx: Context) => Response | Promise<Response>;
|
||||
|
||||
/** Forward-auth response hook. A null user means the request is unauthenticated. */
|
||||
export type AuthSessionVerificationHandler = (
|
||||
ctx: Context,
|
||||
user: AuthPublicUser | null,
|
||||
) => Response | Promise<Response>;
|
||||
|
||||
export interface AuthSuccessfulSignUpAction {
|
||||
/**
|
||||
* Run the normal login policy with the newly registered credentials and
|
||||
|
||||
@@ -51,6 +51,48 @@ test("passwordless and passkey components preserve MFA continuation metadata", (
|
||||
expect(signIn).toContain('name="deviceName"');
|
||||
});
|
||||
|
||||
test("recovery forms expose a form slot for CAPTCHA and policy controls", () => {
|
||||
const source = readFileSync(join(directory, "ForgotPassword.wrn"), "utf8");
|
||||
const formStart = source.indexOf("<form");
|
||||
const formEnd = source.indexOf("</form>");
|
||||
const slot = source.indexOf("<slot></slot>");
|
||||
|
||||
expect(formStart).toBeGreaterThanOrEqual(0);
|
||||
expect(slot).toBeGreaterThan(formStart);
|
||||
expect(slot).toBeLessThan(formEnd);
|
||||
});
|
||||
|
||||
test("account forms use the dedicated password components", () => {
|
||||
const signIn = readFileSync(join(directory, "SignIn.wrn"), "utf8");
|
||||
const signUp = readFileSync(join(directory, "SignUp.wrn"), "utf8");
|
||||
|
||||
expect(signIn).toContain("<TogglePassword");
|
||||
expect(signIn).not.toContain('id="auth-sign-in-password"');
|
||||
expect(signUp).toContain("<StrongPassword");
|
||||
expect(signUp).not.toContain('id="auth-sign-up-password"');
|
||||
});
|
||||
|
||||
test("authentication journeys use themed UI form controls", () => {
|
||||
const expected = {
|
||||
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
|
||||
"SignUp.wrn": ["<Input", "<StrongPassword", "<Checkbox", "<Button"],
|
||||
"ForgotPassword.wrn": ["<Input", "<Button"],
|
||||
"ResetPassword.wrn": ["<StrongPassword", "<Button"],
|
||||
"MagicLinkSignIn.wrn": ["<Input", "<Button"],
|
||||
"InvitationAccept.wrn": ["<Input", "<StrongPassword", "<Button"],
|
||||
"OtpSignIn.wrn": ["<Input", "<PinInput", "<Button"],
|
||||
"TwoFactorChallenge.wrn": ["<Select", "<PinInput", "<Button"],
|
||||
"AuthenticatorSetup.wrn": ["<Input", "<PinInput", "<Button"],
|
||||
"VerifyEmail.wrn": ["<Input", "<Button"],
|
||||
"VerifyPhone.wrn": ["<PinInput", "<Button"],
|
||||
};
|
||||
|
||||
for (const [file, controls] of Object.entries(expected)) {
|
||||
const source = readFileSync(join(directory, file), "utf8");
|
||||
for (const control of controls) expect(source).toContain(control);
|
||||
}
|
||||
});
|
||||
|
||||
test("verification components can resend without application-owned schema files", () => {
|
||||
const email = readFileSync(join(directory, "VerifyEmail.wrn"), "utf8");
|
||||
const phone = readFileSync(join(directory, "VerifyPhone.wrn"), "utf8");
|
||||
|
||||
@@ -178,6 +178,26 @@ test("config.auth registers package routes", async () => {
|
||||
|
||||
expect(contributions.migrations).toHaveLength(0);
|
||||
});
|
||||
test("config.auth contributes a reusable forward-auth verification route", async () => {
|
||||
const runner = createPluginRunner(authPlugin(), {
|
||||
root: process.cwd(),
|
||||
mode: "development",
|
||||
command: "dev",
|
||||
metadata: new Map<string, unknown>(),
|
||||
warn() {},
|
||||
});
|
||||
await runner.configure({
|
||||
auth: {
|
||||
engine: {} as never,
|
||||
routes: true,
|
||||
},
|
||||
});
|
||||
const contributions = await runner.contributions();
|
||||
expect(contributions.routes.some((route) => route.path === "/api/auth/session/verify")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("config.auth can disable route groups", async () => {
|
||||
const metadata = new Map<string, unknown>();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user