fix(auth): secure hydration and align password forms
This commit is contained in:
@@ -212,7 +212,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -332,7 +332,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.11",
|
"version": "0.8.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -617,7 +617,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.10",
|
"version": "0.8.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Global document layout. The framework renders this once around the selected
|
// Global document layout. The framework renders this once around the selected
|
||||||
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
||||||
// Request cookies, resolved theme, language, URL, and pathname are available
|
// Resolved theme, language, URL, and pathname are available as safe SSR props.
|
||||||
// as SSR props, so document attributes do not need a client-side correction.
|
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
|
||||||
theme = "light"
|
theme = "light"
|
||||||
language = "en"
|
language = "en"
|
||||||
url = ""
|
url = ""
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Generated by scripts/generate-showcase.mjs. Do not edit directly.
|
// Generated by scripts/generate-showcase.mjs. Do not edit directly.
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
cookies = {}
|
||||||
theme = "light"
|
theme = "light"
|
||||||
|
|||||||
@@ -1729,6 +1729,8 @@ layout Showcase {
|
|||||||
function documentLayout() {
|
function documentLayout() {
|
||||||
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
|
return `// Generated by scripts/generate-showcase.mjs. Do not edit directly.
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
cookies = {}
|
||||||
theme = "light"
|
theme = "light"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
language: string = "en"
|
language: string = "en"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Global document layout. The framework renders this once around the selected
|
// Global document layout. The framework renders this once around the selected
|
||||||
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
||||||
// Request cookies, resolved theme, language, URL, and pathname are available
|
// Resolved theme, language, URL, and pathname are available as safe SSR props.
|
||||||
// as SSR props, so document attributes do not need a client-side correction.
|
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
|
||||||
theme = "light"
|
theme = "light"
|
||||||
language = "en"
|
language = "en"
|
||||||
url = ""
|
url = ""
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
// Global document layout. The framework renders this once around the selected
|
// Global document layout. The framework renders this once around the selected
|
||||||
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
||||||
// Request cookies, resolved theme, language, URL, and pathname are available
|
// Resolved theme, language, URL, and pathname are available as safe SSR props.
|
||||||
// as SSR props, so document attributes do not need a client-side correction.
|
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
|
||||||
theme = "light"
|
theme = "light"
|
||||||
language = "en"
|
language = "en"
|
||||||
url = ""
|
url = ""
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ component SignIn {
|
|||||||
<input type="hidden" name="deviceName" 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}" />
|
<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">
|
<div class="space-y-1.5">
|
||||||
<div class="flex justify-end"><a href='{forgotHref}' class="text-xs text-[var(--wire-color-primary)] hover:underline">Forgot password?</a></div>
|
|
||||||
<TogglePassword
|
<TogglePassword
|
||||||
label="{passwordLabel}"
|
label="{passwordLabel}"
|
||||||
|
cornerHint="Forgot password?"
|
||||||
|
cornerHref="{forgotHref}"
|
||||||
name="password"
|
name="password"
|
||||||
placeholder="{passwordPlaceholder}"
|
placeholder="{passwordPlaceholder}"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.9",
|
"version": "0.8.10",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -452,11 +452,13 @@ body {
|
|||||||
`,
|
`,
|
||||||
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
"app/layouts/document.wrn": `// Global document layout. The framework renders this once around the selected
|
||||||
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
// page layout and merges SEO metadata, styles, and scripts into <head>/<body>.
|
||||||
// Request cookies, resolved theme, language, URL, and pathname are available
|
// Resolved theme, language, URL, and pathname are available as safe SSR props,
|
||||||
// as SSR props, so document attributes do not need a client-side correction.
|
// so document attributes do not need a client-side correction. Request cookies
|
||||||
|
// remain server-private and are never passed into document hydration.
|
||||||
layout Document {
|
layout Document {
|
||||||
|
runtime = "server"
|
||||||
|
|
||||||
props {
|
props {
|
||||||
cookies = {}
|
|
||||||
theme = "light"
|
theme = "light"
|
||||||
language = "en"
|
language = "en"
|
||||||
url = ""
|
url = ""
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ test("scaffoldApp includes the complete v0.8 configuration and starter structure
|
|||||||
scaffoldApp(root, "complete-app");
|
scaffoldApp(root, "complete-app");
|
||||||
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||||
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
const config = readFileSync(join(root, "wrnexus.config.ts"), "utf8");
|
||||||
|
const documentLayout = readFileSync(join(root, "app/layouts/document.wrn"), "utf8");
|
||||||
|
|
||||||
|
expect(documentLayout).toContain('runtime = "server"');
|
||||||
|
expect(documentLayout).not.toContain("cookies =");
|
||||||
|
|
||||||
for (const packageName of [
|
for (const packageName of [
|
||||||
"@wrnexus/auth",
|
"@wrnexus/auth",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.11",
|
"version": "0.8.12",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1785,7 +1785,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
if (typeof documentRender === "function") {
|
if (typeof documentRender === "function") {
|
||||||
const rendered = String(
|
const rendered = String(
|
||||||
documentRender({
|
documentRender({
|
||||||
cookies: ctx.cookies.getAll(),
|
|
||||||
theme: resolvedTheme,
|
theme: resolvedTheme,
|
||||||
language,
|
language,
|
||||||
url: ctx.url.toString(),
|
url: ctx.url.toString(),
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ component StrongPassword {
|
|||||||
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
|
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
|
||||||
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
|
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
|
||||||
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
|
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
|
||||||
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
|
{#if requireSpecialCharacter}<li class="wire-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
|
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
|
||||||
@@ -205,7 +205,7 @@ component StrongPassword {
|
|||||||
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
|
{#if requireLowercase}<li data-met="{password && hasLowercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One lowercase letter</li>{/if}
|
||||||
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
|
{#if requireUppercase}<li data-met="{password && hasUppercase() ? 'true' : 'false'}"><span aria-hidden="true"></span>One uppercase letter</li>{/if}
|
||||||
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
|
{#if requireNumber}<li data-met="{password && hasNumber() ? 'true' : 'false'}"><span aria-hidden="true"></span>One number</li>{/if}
|
||||||
{#if requireSpecialCharacter}<li data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
|
{#if requireSpecialCharacter}<li class="wire-next__strong-password-special" data-met="{password && hasSpecialCharacter() ? 'true' : 'false'}"><span aria-hidden="true"></span>One special character: {specialCharactersSet}</li>{/if}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
|
{#if hintText}<small id="{name}-hint">{hintText}</small>{/if}
|
||||||
@@ -236,8 +236,8 @@ component StrongPassword {
|
|||||||
|
|
||||||
.wire-next--strong-password input {
|
.wire-next--strong-password input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 2.75em;
|
min-height: 2.65rem;
|
||||||
padding: 0.68em 0.85em;
|
padding: 0.7rem 0.85rem;
|
||||||
border: 1px solid var(--wire-color-border);
|
border: 1px solid var(--wire-color-border);
|
||||||
border-radius: var(--wire-radius-sm);
|
border-radius: var(--wire-radius-sm);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@@ -322,6 +322,15 @@ component StrongPassword {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wire-next__strong-password-requirements .wire-next__strong-password-special {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wire-next__strong-password-special [data-text="specialCharactersSet"] {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.wire-next__strong-password-requirements li > span[aria-hidden="true"] {
|
.wire-next__strong-password-requirements li > span[aria-hidden="true"] {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 0.9rem;
|
width: 0.9rem;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ component TogglePassword {
|
|||||||
invalid: boolean = false
|
invalid: boolean = false
|
||||||
helpText: string = ""
|
helpText: string = ""
|
||||||
validationMessage: string = ""
|
validationMessage: string = ""
|
||||||
|
cornerHint: string = ""
|
||||||
|
cornerHref: string = ""
|
||||||
class: string = ""
|
class: string = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +100,12 @@ component TogglePassword {
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<label for="{name}-password">{label}</label>
|
<div class="wire-next__password-heading">
|
||||||
|
<label for="{name}-password">{label}</label>
|
||||||
|
{#if cornerHint}
|
||||||
|
{#if cornerHref}<a href="{cornerHref}">{cornerHint}</a>{:else}<span>{cornerHint}</span>{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="wire-next__password-control">
|
<div class="wire-next__password-control">
|
||||||
<input
|
<input
|
||||||
{...attrs}
|
{...attrs}
|
||||||
@@ -172,6 +179,28 @@ component TogglePassword {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wire-next__password-heading {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
color: var(--wire-color-text);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wire-next__password-heading :is(a, span) {
|
||||||
|
color: var(--wire-component-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wire-next__password-heading a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.wire-next__password-fields {
|
.wire-next__password-fields {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.10",
|
"version": "0.8.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user