init(nebula): add go templating files and gitignore
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
nebula
|
||||
692
views/register.templ
Normal file
692
views/register.templ
Normal file
@@ -0,0 +1,692 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"nebula/components"
|
||||
"nebula/layouts"
|
||||
)
|
||||
|
||||
var registerSteps = []string{"Detect", "Register", "Complete"}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// DeviceCapabilities holds the WebAuthn capability detection results
|
||||
type DeviceCapabilities struct {
|
||||
Platform bool // Biometrics (Face ID, Touch ID, Windows Hello)
|
||||
CrossPlatform bool // Security keys (YubiKey, etc.)
|
||||
Conditional bool // Passkey autofill support
|
||||
}
|
||||
|
||||
// RegisterState holds the current registration state
|
||||
type RegisterState struct {
|
||||
Step int
|
||||
Method string // "passkey", "security-key", or "qr-code"
|
||||
Username string
|
||||
Error string
|
||||
}
|
||||
|
||||
// RegisterPage renders the full registration page with the specified step
|
||||
templ RegisterPage(state RegisterState) {
|
||||
@layouts.CenteredCard("Register - Sonr") {
|
||||
<div slot="header" id="stepper-container" hx-swap-oob:inherited="true">
|
||||
@components.OnboardingStepper(state.Step, registerSteps)
|
||||
</div>
|
||||
<div id="step-content" class="step-content">
|
||||
@RegisterStepContent(state)
|
||||
</div>
|
||||
<div id="htmx-indicator" class="htmx-indicator">
|
||||
<wa-spinner></wa-spinner>
|
||||
</div>
|
||||
<footer slot="footer">
|
||||
@RegisterFooter()
|
||||
</footer>
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterStepContent renders the content for a specific step (used for HTMX partials)
|
||||
templ RegisterStepContent(state RegisterState) {
|
||||
switch state.Step {
|
||||
case 1:
|
||||
@RegisterStep1()
|
||||
case 2:
|
||||
switch state.Method {
|
||||
case "passkey":
|
||||
@RegisterStep2Passkey()
|
||||
case "security-key":
|
||||
@RegisterStep2SecurityKey()
|
||||
case "qr-code":
|
||||
@RegisterStep2QRCode()
|
||||
default:
|
||||
@RegisterStep2Passkey()
|
||||
}
|
||||
case 3:
|
||||
@RegisterStep3()
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterStepWithStepper renders step content with OOB stepper update for HTMX 4
|
||||
templ RegisterStepWithStepper(state RegisterState) {
|
||||
@RegisterStepContent(state)
|
||||
<div id="stepper-container" hx-swap-oob="innerHTML">
|
||||
@components.OnboardingStepper(state.Step, registerSteps)
|
||||
</div>
|
||||
}
|
||||
|
||||
// RegisterStep1 - Device Detection and Method Selection
|
||||
templ RegisterStep1() {
|
||||
@registerStyles()
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">Create Your Account</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Setting up secure sign-in...
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
id="capabilities"
|
||||
class="capability-list"
|
||||
hx-get="/register/capabilities"
|
||||
hx-trigger="load delay:500ms"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
@CapabilityItem("cap-platform", "fingerprint", "Face or Fingerprint", false, true)
|
||||
@CapabilityItem("cap-cross-platform", "key", "Hardware Security Key", false, true)
|
||||
@CapabilityItem("cap-conditional", "bolt", "Quick Sign-in", false, true)
|
||||
</div>
|
||||
<wa-divider></wa-divider>
|
||||
<wa-radio-group
|
||||
label="How do you want to sign in?"
|
||||
orientation="vertical"
|
||||
name="auth-method"
|
||||
id="auth-method-group"
|
||||
>
|
||||
<wa-radio appearance="button" value="passkey" id="radio-passkey" disabled>
|
||||
Use Face or Fingerprint (Recommended)
|
||||
</wa-radio>
|
||||
<wa-radio appearance="button" value="security-key" id="radio-security-key" disabled>
|
||||
Use a Hardware Key
|
||||
</wa-radio>
|
||||
<wa-radio appearance="button" value="qr-code" id="radio-qr">
|
||||
Use Another Device
|
||||
</wa-radio>
|
||||
</wa-radio-group>
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button
|
||||
id="btn-continue"
|
||||
variant="brand"
|
||||
disabled
|
||||
hx-get="/register/step/2"
|
||||
hx-target="#step-content"
|
||||
hx-swap="innerHTML transition:true"
|
||||
hx-indicator="#htmx-indicator"
|
||||
hx-include="#auth-method-group"
|
||||
>
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
@registerStep1Scripts()
|
||||
}
|
||||
|
||||
// CapabilityItem renders a single capability detection item
|
||||
templ CapabilityItem(id string, icon string, label string, supported bool, loading bool) {
|
||||
<div
|
||||
class={ "capability-item", templ.KV("supported", supported && !loading), templ.KV("unsupported", !supported && !loading) }
|
||||
id={ id }
|
||||
>
|
||||
if loading {
|
||||
<wa-spinner style="--size: 1.5rem;"></wa-spinner>
|
||||
} else if supported {
|
||||
<wa-icon name="circle-check" style="color: var(--wa-color-success); font-size: 1.5rem;"></wa-icon>
|
||||
} else {
|
||||
<wa-icon name="circle-xmark" style="color: var(--wa-color-danger); font-size: 1.5rem;"></wa-icon>
|
||||
}
|
||||
<span>{ label }</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ CapabilitiesResult(caps DeviceCapabilities) {
|
||||
@CapabilityItem("cap-platform", "fingerprint", "Face or Fingerprint", caps.Platform, false)
|
||||
@CapabilityItem("cap-cross-platform", "key", "Hardware Security Key", caps.CrossPlatform, false)
|
||||
@CapabilityItem("cap-conditional", "bolt", "Quick Sign-in", caps.Conditional, false)
|
||||
@capabilitiesScript(caps)
|
||||
}
|
||||
|
||||
templ capabilitiesScript(caps DeviceCapabilities) {
|
||||
<div
|
||||
id="caps-data"
|
||||
data-platform={ boolStr(caps.Platform) }
|
||||
data-cross-platform={ boolStr(caps.CrossPlatform) }
|
||||
data-conditional={ boolStr(caps.Conditional) }
|
||||
style="display:none;"
|
||||
></div>
|
||||
<script>
|
||||
(function() {
|
||||
const capsData = document.getElementById('caps-data');
|
||||
const hasPlatform = capsData?.dataset.platform === 'true';
|
||||
const hasCrossPlatform = capsData?.dataset.crossPlatform === 'true';
|
||||
|
||||
const radioPasskey = document.getElementById('radio-passkey');
|
||||
const radioSecurityKey = document.getElementById('radio-security-key');
|
||||
const btnContinue = document.getElementById('btn-continue');
|
||||
const methodGroup = document.getElementById('auth-method-group');
|
||||
|
||||
if (radioPasskey && hasPlatform) {
|
||||
radioPasskey.removeAttribute('disabled');
|
||||
}
|
||||
if (radioSecurityKey && hasCrossPlatform) {
|
||||
radioSecurityKey.removeAttribute('disabled');
|
||||
}
|
||||
|
||||
if (methodGroup) {
|
||||
if (hasPlatform) {
|
||||
methodGroup.value = 'passkey';
|
||||
} else if (hasCrossPlatform) {
|
||||
methodGroup.value = 'security-key';
|
||||
} else {
|
||||
methodGroup.value = 'qr-code';
|
||||
}
|
||||
|
||||
if (btnContinue) {
|
||||
btnContinue.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
// RegisterStep2Passkey - Passkey Registration Form
|
||||
templ RegisterStep2Passkey() {
|
||||
@registerStyles()
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">Set Up Face or Fingerprint</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Sign in securely using your face or fingerprint.
|
||||
</p>
|
||||
</div>
|
||||
<form id="passkey-form" class="wa-stack wa-gap-m">
|
||||
<wa-input
|
||||
name="username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
required
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
|
||||
</wa-input>
|
||||
<wa-input
|
||||
name="display-name"
|
||||
type="text"
|
||||
label="Display Name"
|
||||
placeholder="Your name (optional)"
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="id-card"></wa-icon>
|
||||
</wa-input>
|
||||
</form>
|
||||
<wa-alert variant="info" open>
|
||||
<wa-icon slot="icon" name="fingerprint"></wa-icon>
|
||||
<strong>Next: Verify your identity</strong>
|
||||
<br/>
|
||||
Your device will ask for your face, fingerprint, or PIN.
|
||||
</wa-alert>
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button
|
||||
appearance="outlined"
|
||||
variant="neutral"
|
||||
hx-get="/register/step/1"
|
||||
hx-target="#step-content"
|
||||
hx-swap="innerHTML transition:true"
|
||||
hx-indicator="#htmx-indicator"
|
||||
>
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button
|
||||
variant="brand"
|
||||
id="btn-register-passkey"
|
||||
>
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="fingerprint"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
@passkeyRegistrationScript()
|
||||
}
|
||||
|
||||
// RegisterStep2SecurityKey - Security Key Registration Form
|
||||
templ RegisterStep2SecurityKey() {
|
||||
@registerStyles()
|
||||
<div class="wa-stack wa-gap-l">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">Set Up Hardware Key</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Use a physical security key like YubiKey to sign in.
|
||||
</p>
|
||||
</div>
|
||||
<form id="security-key-form" class="wa-stack wa-gap-m">
|
||||
<wa-input
|
||||
name="username"
|
||||
type="text"
|
||||
label="Username"
|
||||
placeholder="Choose a username"
|
||||
required
|
||||
>
|
||||
<wa-icon slot="end" variant="regular" name="user"></wa-icon>
|
||||
</wa-input>
|
||||
</form>
|
||||
<wa-alert variant="info" open>
|
||||
<wa-icon slot="icon" name="key"></wa-icon>
|
||||
<strong>Plug in your security key</strong>
|
||||
<br/>
|
||||
After clicking Continue, tap the button on your key when it blinks.
|
||||
</wa-alert>
|
||||
<div class="wa-cluster wa-justify-content-end wa-gap-s">
|
||||
<wa-button
|
||||
appearance="outlined"
|
||||
variant="neutral"
|
||||
hx-get="/register/step/1"
|
||||
hx-target="#step-content"
|
||||
hx-swap="innerHTML transition:true"
|
||||
hx-indicator="#htmx-indicator"
|
||||
>
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button
|
||||
variant="brand"
|
||||
id="btn-register-security-key"
|
||||
>
|
||||
Continue
|
||||
<wa-icon slot="end" variant="regular" name="key"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
@securityKeyRegistrationScript()
|
||||
}
|
||||
|
||||
// RegisterStep2QRCode - QR Code Fallback Registration
|
||||
templ RegisterStep2QRCode() {
|
||||
@registerStyles()
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">Use Your Phone</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Scan this code with your phone camera to continue setup
|
||||
</p>
|
||||
</div>
|
||||
<wa-qr-code
|
||||
value="https://sonr.id/register?token=abc123xyz789"
|
||||
label="Scan with your phone camera"
|
||||
></wa-qr-code>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Or copy this link to your phone</p>
|
||||
<wa-input
|
||||
value="sonr.id/r/abc123xyz789"
|
||||
disabled
|
||||
style="width: 100%;"
|
||||
>
|
||||
<wa-copy-button slot="end" value="sonr.id/r/abc123xyz789"></wa-copy-button>
|
||||
</wa-input>
|
||||
<wa-divider></wa-divider>
|
||||
<div class="wa-stack wa-gap-s wa-align-items-center" style="width: 100%;">
|
||||
<h3 class="wa-heading-s">Enter Code from Phone</h3>
|
||||
<p class="wa-caption-s" style="color: var(--wa-color-neutral-500);">
|
||||
After scanning, enter the 6-digit code shown on your phone.
|
||||
</p>
|
||||
<wa-input
|
||||
type="text"
|
||||
placeholder="000000"
|
||||
maxlength="6"
|
||||
id="verification-code"
|
||||
name="code"
|
||||
class="verification-code-input"
|
||||
></wa-input>
|
||||
</div>
|
||||
<div class="wa-cluster wa-justify-content-center wa-gap-s" style="width: 100%;">
|
||||
<wa-button
|
||||
appearance="outlined"
|
||||
variant="neutral"
|
||||
hx-get="/register/step/1"
|
||||
hx-target="#step-content"
|
||||
hx-swap="innerHTML transition:true"
|
||||
hx-indicator="#htmx-indicator"
|
||||
>
|
||||
Back
|
||||
</wa-button>
|
||||
<wa-button
|
||||
variant="brand"
|
||||
id="btn-verify-code"
|
||||
hx-post="/register/verify-code"
|
||||
hx-target="#step-content"
|
||||
hx-swap="innerHTML transition:true"
|
||||
hx-indicator="#htmx-indicator"
|
||||
hx-include="#verification-code"
|
||||
>
|
||||
Verify
|
||||
</wa-button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
// RegisterStep3 - Success with Recovery Keys
|
||||
templ RegisterStep3() {
|
||||
@registerStyles()
|
||||
<div class="wa-stack wa-gap-l wa-align-items-center">
|
||||
<wa-icon name="circle-check" family="duotone" class="success-icon"></wa-icon>
|
||||
<div class="wa-stack wa-gap-xs" style="text-align: center;">
|
||||
<h2 class="wa-heading-l">You're All Set</h2>
|
||||
<p class="wa-caption-m" style="color: var(--wa-color-neutral-600);">
|
||||
Your account is ready. Save your backup codes before continuing.
|
||||
</p>
|
||||
</div>
|
||||
<wa-alert variant="warning" open>
|
||||
<wa-icon slot="icon" name="shield-exclamation"></wa-icon>
|
||||
<strong>Important: Save these codes now</strong>
|
||||
<br/>
|
||||
If you lose your device, these codes are the only way to recover your account.
|
||||
</wa-alert>
|
||||
<div class="wa-stack wa-gap-m" style="width: 100%;">
|
||||
<wa-input label="Account ID" value="pk_live_a1b2c3d4e5f6g7h8i9j0" disabled>
|
||||
<wa-copy-button slot="end" value="pk_live_a1b2c3d4e5f6g7h8i9j0"></wa-copy-button>
|
||||
</wa-input>
|
||||
<wa-input label="Backup Code" value="rk_sec_z9y8x7w6v5u4t3s2r1q0" disabled>
|
||||
<wa-copy-button slot="end" value="rk_sec_z9y8x7w6v5u4t3s2r1q0"></wa-copy-button>
|
||||
</wa-input>
|
||||
</div>
|
||||
<wa-checkbox id="confirm-saved" required>
|
||||
I saved these codes somewhere safe
|
||||
</wa-checkbox>
|
||||
<wa-button
|
||||
variant="brand"
|
||||
id="btn-finish"
|
||||
disabled
|
||||
style="width: 100%;"
|
||||
onclick="window.location.href='/dashboard'"
|
||||
>
|
||||
Go to Dashboard
|
||||
<wa-icon slot="end" variant="regular" name="arrow-right"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
@step3Scripts()
|
||||
}
|
||||
|
||||
// RegisterFooter renders the footer with login link
|
||||
templ RegisterFooter() {
|
||||
<div class="wa-cluster wa-justify-content-center wa-gap-m">
|
||||
<span class="wa-caption-s" style="color: var(--wa-color-neutral-500);">Already have an account?</span>
|
||||
<wa-button appearance="plain" size="small" onclick="window.location.href='/login'">
|
||||
Sign In
|
||||
</wa-button>
|
||||
</div>
|
||||
}
|
||||
|
||||
// registerStyles contains the CSS specific to the register page
|
||||
templ registerStyles() {
|
||||
<style>
|
||||
/* HTMX 4 indicator and transition styles */
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 100;
|
||||
}
|
||||
.htmx-request .htmx-indicator {
|
||||
display: flex;
|
||||
}
|
||||
.htmx-request.step-content {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
/* View transition support for HTMX 4 */
|
||||
@view-transition {
|
||||
navigation: auto;
|
||||
}
|
||||
::view-transition-old(step-content),
|
||||
::view-transition-new(step-content) {
|
||||
animation-duration: 0.25s;
|
||||
}
|
||||
.step-content {
|
||||
view-transition-name: step-content;
|
||||
position: relative;
|
||||
}
|
||||
/* Device capability badges */
|
||||
.capability-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-s);
|
||||
}
|
||||
.capability-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--wa-space-s);
|
||||
padding: var(--wa-space-s) var(--wa-space-m);
|
||||
border-radius: var(--wa-radius-m);
|
||||
background: var(--wa-color-surface-alt);
|
||||
border-left: 3px solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.capability-item.supported {
|
||||
border-left-color: var(--wa-color-success);
|
||||
}
|
||||
.capability-item.unsupported {
|
||||
border-left-color: var(--wa-color-danger);
|
||||
opacity: 0.6;
|
||||
}
|
||||
/* Success icon */
|
||||
.success-icon {
|
||||
font-size: 4rem;
|
||||
color: var(--wa-color-success);
|
||||
}
|
||||
/* Verification code input styling */
|
||||
.verification-code-input {
|
||||
text-align: center;
|
||||
font-size: 1.5rem;
|
||||
letter-spacing: 0.3em;
|
||||
max-width: 180px;
|
||||
}
|
||||
/* Radio group styling */
|
||||
wa-radio-group[orientation="vertical"] {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--wa-space-s);
|
||||
}
|
||||
wa-radio[appearance="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
wa-radio[appearance="button"][disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* Viewport constraints for popup/webview */
|
||||
@media (max-width: 400px) {
|
||||
.capability-item {
|
||||
padding: var(--wa-space-xs) var(--wa-space-s);
|
||||
font-size: var(--wa-font-size-s);
|
||||
}
|
||||
.verification-code-input {
|
||||
font-size: 1.25rem;
|
||||
max-width: 160px;
|
||||
}
|
||||
.success-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
@media (max-height: 600px) {
|
||||
.capability-item {
|
||||
padding: var(--wa-space-xs) var(--wa-space-s);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
}
|
||||
|
||||
// registerStep1Scripts handles method selection and continue button enabling
|
||||
templ registerStep1Scripts() {
|
||||
<script>
|
||||
(function() {
|
||||
const methodGroup = document.getElementById('auth-method-group');
|
||||
const btnContinue = document.getElementById('btn-continue');
|
||||
|
||||
if (methodGroup && btnContinue) {
|
||||
// Listen for method selection changes
|
||||
methodGroup.addEventListener('wa-change', (e) => {
|
||||
const selectedMethod = e.target.value;
|
||||
if (selectedMethod) {
|
||||
btnContinue.removeAttribute('disabled');
|
||||
// Update the hx-get URL with selected method
|
||||
btnContinue.setAttribute('hx-get', '/register/step/2?method=' + selectedMethod);
|
||||
htmx.process(btnContinue);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
// passkeyRegistrationScript handles WebAuthn passkey registration
|
||||
templ passkeyRegistrationScript() {
|
||||
<script>
|
||||
(function() {
|
||||
const btn = document.getElementById('btn-register-passkey');
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
try {
|
||||
const username = document.querySelector('[name="username"]')?.value;
|
||||
const displayName = document.querySelector('[name="display-name"]')?.value || username;
|
||||
|
||||
if (!username) {
|
||||
alert('Please enter a username');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get challenge from server (in production)
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const createOptions = {
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rp: {
|
||||
name: "Sonr",
|
||||
id: window.location.hostname
|
||||
},
|
||||
user: {
|
||||
id: new Uint8Array(16),
|
||||
name: username,
|
||||
displayName: displayName
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 }, // ES256
|
||||
{ type: "public-key", alg: -257 } // RS256
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "platform",
|
||||
userVerification: "required",
|
||||
residentKey: "required"
|
||||
},
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create(createOptions);
|
||||
console.log("Passkey created:", credential);
|
||||
|
||||
// Navigate to success step
|
||||
htmx.ajax('GET', '/register/step/3', {
|
||||
target: '#step-content',
|
||||
swap: 'innerHTML transition:true'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Passkey registration failed:", error);
|
||||
alert("Registration failed: " + error.message);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
// securityKeyRegistrationScript handles WebAuthn security key registration
|
||||
templ securityKeyRegistrationScript() {
|
||||
<script>
|
||||
(function() {
|
||||
const btn = document.getElementById('btn-register-security-key');
|
||||
if (!btn) return;
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
try {
|
||||
const username = document.querySelector('[name="username"]')?.value;
|
||||
|
||||
if (!username) {
|
||||
alert('Please enter a username');
|
||||
return;
|
||||
}
|
||||
|
||||
const challenge = new Uint8Array(32);
|
||||
crypto.getRandomValues(challenge);
|
||||
|
||||
const createOptions = {
|
||||
publicKey: {
|
||||
challenge: challenge,
|
||||
rp: {
|
||||
name: "Sonr",
|
||||
id: window.location.hostname
|
||||
},
|
||||
user: {
|
||||
id: new Uint8Array(16),
|
||||
name: username,
|
||||
displayName: username
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
{ type: "public-key", alg: -7 },
|
||||
{ type: "public-key", alg: -257 }
|
||||
],
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: "cross-platform",
|
||||
userVerification: "preferred"
|
||||
},
|
||||
timeout: 60000
|
||||
}
|
||||
};
|
||||
|
||||
const credential = await navigator.credentials.create(createOptions);
|
||||
console.log("Security key registered:", credential);
|
||||
|
||||
// Navigate to success step
|
||||
htmx.ajax('GET', '/register/step/3', {
|
||||
target: '#step-content',
|
||||
swap: 'innerHTML transition:true'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Security key registration failed:", error);
|
||||
alert("Registration failed: " + error.message);
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
// step3Scripts handles the confirmation checkbox and finish button
|
||||
templ step3Scripts() {
|
||||
<script>
|
||||
(function() {
|
||||
const confirmCheckbox = document.getElementById('confirm-saved');
|
||||
const finishBtn = document.getElementById('btn-finish');
|
||||
|
||||
if (confirmCheckbox && finishBtn) {
|
||||
confirmCheckbox.addEventListener('wa-change', (e) => {
|
||||
if (e.target.checked) {
|
||||
finishBtn.removeAttribute('disabled');
|
||||
} else {
|
||||
finishBtn.setAttribute('disabled', '');
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
800
views/register_templ.go
Normal file
800
views/register_templ.go
Normal file
@@ -0,0 +1,800 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.977
|
||||
package views
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import (
|
||||
"nebula/components"
|
||||
"nebula/layouts"
|
||||
)
|
||||
|
||||
var registerSteps = []string{"Detect", "Register", "Complete"}
|
||||
|
||||
func boolStr(b bool) string {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
// DeviceCapabilities holds the WebAuthn capability detection results
|
||||
type DeviceCapabilities struct {
|
||||
Platform bool // Biometrics (Face ID, Touch ID, Windows Hello)
|
||||
CrossPlatform bool // Security keys (YubiKey, etc.)
|
||||
Conditional bool // Passkey autofill support
|
||||
}
|
||||
|
||||
// RegisterState holds the current registration state
|
||||
type RegisterState struct {
|
||||
Step int
|
||||
Method string // "passkey", "security-key", or "qr-code"
|
||||
Username string
|
||||
Error string
|
||||
}
|
||||
|
||||
// RegisterPage renders the full registration page with the specified step
|
||||
func RegisterPage(state RegisterState) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div slot=\"header\" id=\"stepper-container\" hx-swap-oob:inherited=\"true\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.OnboardingStepper(state.Step, registerSteps).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div id=\"step-content\" class=\"step-content\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = RegisterStepContent(state).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div id=\"htmx-indicator\" class=\"htmx-indicator\"><wa-spinner></wa-spinner></div><footer slot=\"footer\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = RegisterFooter().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</footer>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = layouts.CenteredCard("Register - Sonr").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStepContent renders the content for a specific step (used for HTMX partials)
|
||||
func RegisterStepContent(state RegisterState) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
switch state.Step {
|
||||
case 1:
|
||||
templ_7745c5c3_Err = RegisterStep1().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case 2:
|
||||
switch state.Method {
|
||||
case "passkey":
|
||||
templ_7745c5c3_Err = RegisterStep2Passkey().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "security-key":
|
||||
templ_7745c5c3_Err = RegisterStep2SecurityKey().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
case "qr-code":
|
||||
templ_7745c5c3_Err = RegisterStep2QRCode().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
default:
|
||||
templ_7745c5c3_Err = RegisterStep2Passkey().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
case 3:
|
||||
templ_7745c5c3_Err = RegisterStep3().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStepWithStepper renders step content with OOB stepper update for HTMX 4
|
||||
func RegisterStepWithStepper(state RegisterState) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var4 == nil {
|
||||
templ_7745c5c3_Var4 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = RegisterStepContent(state).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div id=\"stepper-container\" hx-swap-oob=\"innerHTML\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = components.OnboardingStepper(state.Step, registerSteps).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStep1 - Device Detection and Method Selection
|
||||
func RegisterStep1() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var5 == nil {
|
||||
templ_7745c5c3_Var5 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = registerStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"wa-stack wa-gap-l\"><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">Create Your Account</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Setting up secure sign-in...</p></div><div id=\"capabilities\" class=\"capability-list\" hx-get=\"/register/capabilities\" hx-trigger=\"load delay:500ms\" hx-swap=\"innerHTML\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-platform", "fingerprint", "Face or Fingerprint", false, true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-cross-platform", "key", "Hardware Security Key", false, true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-conditional", "bolt", "Quick Sign-in", false, true).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><wa-divider></wa-divider> <wa-radio-group label=\"How do you want to sign in?\" orientation=\"vertical\" name=\"auth-method\" id=\"auth-method-group\"><wa-radio appearance=\"button\" value=\"passkey\" id=\"radio-passkey\" disabled>Use Face or Fingerprint (Recommended)</wa-radio> <wa-radio appearance=\"button\" value=\"security-key\" id=\"radio-security-key\" disabled>Use a Hardware Key</wa-radio> <wa-radio appearance=\"button\" value=\"qr-code\" id=\"radio-qr\">Use Another Device</wa-radio></wa-radio-group><div class=\"wa-cluster wa-justify-content-end wa-gap-s\"><wa-button id=\"btn-continue\" variant=\"brand\" disabled hx-get=\"/register/step/2\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\" hx-include=\"#auth-method-group\">Continue <wa-icon slot=\"end\" variant=\"regular\" name=\"arrow-right\"></wa-icon></wa-button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = registerStep1Scripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CapabilityItem renders a single capability detection item
|
||||
func CapabilityItem(id string, icon string, label string, supported bool, loading bool) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var7 = []any{"capability-item", templ.KV("supported", supported && !loading), templ.KV("unsupported", !supported && !loading)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var7...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var7).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 140, Col: 9}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if loading {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<wa-spinner style=\"--size: 1.5rem;\"></wa-spinner> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if supported {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<wa-icon name=\"circle-check\" style=\"color: var(--wa-color-success); font-size: 1.5rem;\"></wa-icon> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<wa-icon name=\"circle-xmark\" style=\"color: var(--wa-color-danger); font-size: 1.5rem;\"></wa-icon> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 149, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func CapabilitiesResult(caps DeviceCapabilities) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-platform", "fingerprint", "Face or Fingerprint", caps.Platform, false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-cross-platform", "key", "Hardware Security Key", caps.CrossPlatform, false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = CapabilityItem("cap-conditional", "bolt", "Quick Sign-in", caps.Conditional, false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = capabilitiesScript(caps).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func capabilitiesScript(caps DeviceCapabilities) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var12 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var12 == nil {
|
||||
templ_7745c5c3_Var12 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div id=\"caps-data\" data-platform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(boolStr(caps.Platform))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 163, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" data-cross-platform=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(boolStr(caps.CrossPlatform))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 164, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" data-conditional=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(boolStr(caps.Conditional))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/register.templ`, Line: 165, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" style=\"display:none;\"></div><script>\n\t\t(function() {\n\t\t\tconst capsData = document.getElementById('caps-data');\n\t\t\tconst hasPlatform = capsData?.dataset.platform === 'true';\n\t\t\tconst hasCrossPlatform = capsData?.dataset.crossPlatform === 'true';\n\t\t\t\n\t\t\tconst radioPasskey = document.getElementById('radio-passkey');\n\t\t\tconst radioSecurityKey = document.getElementById('radio-security-key');\n\t\t\tconst btnContinue = document.getElementById('btn-continue');\n\t\t\tconst methodGroup = document.getElementById('auth-method-group');\n\t\t\t\n\t\t\tif (radioPasskey && hasPlatform) {\n\t\t\t\tradioPasskey.removeAttribute('disabled');\n\t\t\t}\n\t\t\tif (radioSecurityKey && hasCrossPlatform) {\n\t\t\t\tradioSecurityKey.removeAttribute('disabled');\n\t\t\t}\n\t\t\t\n\t\t\tif (methodGroup) {\n\t\t\t\tif (hasPlatform) {\n\t\t\t\t\tmethodGroup.value = 'passkey';\n\t\t\t\t} else if (hasCrossPlatform) {\n\t\t\t\t\tmethodGroup.value = 'security-key';\n\t\t\t\t} else {\n\t\t\t\t\tmethodGroup.value = 'qr-code';\n\t\t\t\t}\n\t\t\t\t\n\t\t\t\tif (btnContinue) {\n\t\t\t\t\tbtnContinue.removeAttribute('disabled');\n\t\t\t\t}\n\t\t\t}\n\t\t})();\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStep2Passkey - Passkey Registration Form
|
||||
func RegisterStep2Passkey() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = registerStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"wa-stack wa-gap-l\"><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">Set Up Face or Fingerprint</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Sign in securely using your face or fingerprint.</p></div><form id=\"passkey-form\" class=\"wa-stack wa-gap-m\"><wa-input name=\"username\" type=\"text\" label=\"Username\" placeholder=\"Choose a username\" required><wa-icon slot=\"end\" variant=\"regular\" name=\"user\"></wa-icon></wa-input> <wa-input name=\"display-name\" type=\"text\" label=\"Display Name\" placeholder=\"Your name (optional)\"><wa-icon slot=\"end\" variant=\"regular\" name=\"id-card\"></wa-icon></wa-input></form><wa-alert variant=\"info\" open><wa-icon slot=\"icon\" name=\"fingerprint\"></wa-icon> <strong>Next: Verify your identity</strong><br>Your device will ask for your face, fingerprint, or PIN.</wa-alert><div class=\"wa-cluster wa-justify-content-end wa-gap-s\"><wa-button appearance=\"outlined\" variant=\"neutral\" hx-get=\"/register/step/1\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">Back</wa-button> <wa-button variant=\"brand\" id=\"btn-register-passkey\">Continue <wa-icon slot=\"end\" variant=\"regular\" name=\"fingerprint\"></wa-icon></wa-button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = passkeyRegistrationScript().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStep2SecurityKey - Security Key Registration Form
|
||||
func RegisterStep2SecurityKey() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var17 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var17 == nil {
|
||||
templ_7745c5c3_Var17 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = registerStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"wa-stack wa-gap-l\"><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">Set Up Hardware Key</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Use a physical security key like YubiKey to sign in.</p></div><form id=\"security-key-form\" class=\"wa-stack wa-gap-m\"><wa-input name=\"username\" type=\"text\" label=\"Username\" placeholder=\"Choose a username\" required><wa-icon slot=\"end\" variant=\"regular\" name=\"user\"></wa-icon></wa-input></form><wa-alert variant=\"info\" open><wa-icon slot=\"icon\" name=\"key\"></wa-icon> <strong>Plug in your security key</strong><br>After clicking Continue, tap the button on your key when it blinks.</wa-alert><div class=\"wa-cluster wa-justify-content-end wa-gap-s\"><wa-button appearance=\"outlined\" variant=\"neutral\" hx-get=\"/register/step/1\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">Back</wa-button> <wa-button variant=\"brand\" id=\"btn-register-security-key\">Continue <wa-icon slot=\"end\" variant=\"regular\" name=\"key\"></wa-icon></wa-button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = securityKeyRegistrationScript().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStep2QRCode - QR Code Fallback Registration
|
||||
func RegisterStep2QRCode() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var18 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var18 == nil {
|
||||
templ_7745c5c3_Var18 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = registerStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"wa-stack wa-gap-l wa-align-items-center\"><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">Use Your Phone</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Scan this code with your phone camera to continue setup</p></div><wa-qr-code value=\"https://sonr.id/register?token=abc123xyz789\" label=\"Scan with your phone camera\"></wa-qr-code><p class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">Or copy this link to your phone</p><wa-input value=\"sonr.id/r/abc123xyz789\" disabled style=\"width: 100%;\"><wa-copy-button slot=\"end\" value=\"sonr.id/r/abc123xyz789\"></wa-copy-button></wa-input> <wa-divider></wa-divider><div class=\"wa-stack wa-gap-s wa-align-items-center\" style=\"width: 100%;\"><h3 class=\"wa-heading-s\">Enter Code from Phone</h3><p class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">After scanning, enter the 6-digit code shown on your phone.</p><wa-input type=\"text\" placeholder=\"000000\" maxlength=\"6\" id=\"verification-code\" name=\"code\" class=\"verification-code-input\"></wa-input></div><div class=\"wa-cluster wa-justify-content-center wa-gap-s\" style=\"width: 100%;\"><wa-button appearance=\"outlined\" variant=\"neutral\" hx-get=\"/register/step/1\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\">Back</wa-button> <wa-button variant=\"brand\" id=\"btn-verify-code\" hx-post=\"/register/verify-code\" hx-target=\"#step-content\" hx-swap=\"innerHTML transition:true\" hx-indicator=\"#htmx-indicator\" hx-include=\"#verification-code\">Verify</wa-button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterStep3 - Success with Recovery Keys
|
||||
func RegisterStep3() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var19 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var19 == nil {
|
||||
templ_7745c5c3_Var19 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = registerStyles().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<div class=\"wa-stack wa-gap-l wa-align-items-center\"><wa-icon name=\"circle-check\" family=\"duotone\" class=\"success-icon\"></wa-icon><div class=\"wa-stack wa-gap-xs\" style=\"text-align: center;\"><h2 class=\"wa-heading-l\">You're All Set</h2><p class=\"wa-caption-m\" style=\"color: var(--wa-color-neutral-600);\">Your account is ready. Save your backup codes before continuing.</p></div><wa-alert variant=\"warning\" open><wa-icon slot=\"icon\" name=\"shield-exclamation\"></wa-icon> <strong>Important: Save these codes now</strong><br>If you lose your device, these codes are the only way to recover your account.</wa-alert><div class=\"wa-stack wa-gap-m\" style=\"width: 100%;\"><wa-input label=\"Account ID\" value=\"pk_live_a1b2c3d4e5f6g7h8i9j0\" disabled><wa-copy-button slot=\"end\" value=\"pk_live_a1b2c3d4e5f6g7h8i9j0\"></wa-copy-button></wa-input> <wa-input label=\"Backup Code\" value=\"rk_sec_z9y8x7w6v5u4t3s2r1q0\" disabled><wa-copy-button slot=\"end\" value=\"rk_sec_z9y8x7w6v5u4t3s2r1q0\"></wa-copy-button></wa-input></div><wa-checkbox id=\"confirm-saved\" required>I saved these codes somewhere safe</wa-checkbox> <wa-button variant=\"brand\" id=\"btn-finish\" disabled style=\"width: 100%;\" onclick=\"window.location.href='/dashboard'\">Go to Dashboard <wa-icon slot=\"end\" variant=\"regular\" name=\"arrow-right\"></wa-icon></wa-button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = step3Scripts().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterFooter renders the footer with login link
|
||||
func RegisterFooter() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var20 == nil {
|
||||
templ_7745c5c3_Var20 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"wa-cluster wa-justify-content-center wa-gap-m\"><span class=\"wa-caption-s\" style=\"color: var(--wa-color-neutral-500);\">Already have an account?</span> <wa-button appearance=\"plain\" size=\"small\" onclick=\"window.location.href='/login'\">Sign In</wa-button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// registerStyles contains the CSS specific to the register page
|
||||
func registerStyles() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var21 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var21 == nil {
|
||||
templ_7745c5c3_Var21 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<style>\n\t\t/* HTMX 4 indicator and transition styles */\n\t\t.htmx-indicator {\n\t\t\tdisplay: none;\n\t\t\tposition: absolute;\n\t\t\ttop: 50%;\n\t\t\tleft: 50%;\n\t\t\ttransform: translate(-50%, -50%);\n\t\t\tz-index: 100;\n\t\t}\n\t\t.htmx-request .htmx-indicator {\n\t\t\tdisplay: flex;\n\t\t}\n\t\t.htmx-request.step-content {\n\t\t\topacity: 0.5;\n\t\t\tpointer-events: none;\n\t\t\ttransition: opacity 0.2s ease;\n\t\t}\n\t\t/* View transition support for HTMX 4 */\n\t\t@view-transition {\n\t\t\tnavigation: auto;\n\t\t}\n\t\t::view-transition-old(step-content),\n\t\t::view-transition-new(step-content) {\n\t\t\tanimation-duration: 0.25s;\n\t\t}\n\t\t.step-content {\n\t\t\tview-transition-name: step-content;\n\t\t\tposition: relative;\n\t\t}\n\t\t/* Device capability badges */\n\t\t.capability-list {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: var(--wa-space-s);\n\t\t}\n\t\t.capability-item {\n\t\t\tdisplay: flex;\n\t\t\talign-items: center;\n\t\t\tgap: var(--wa-space-s);\n\t\t\tpadding: var(--wa-space-s) var(--wa-space-m);\n\t\t\tborder-radius: var(--wa-radius-m);\n\t\t\tbackground: var(--wa-color-surface-alt);\n\t\t\tborder-left: 3px solid transparent;\n\t\t\ttransition: all 0.3s ease;\n\t\t}\n\t\t.capability-item.supported {\n\t\t\tborder-left-color: var(--wa-color-success);\n\t\t}\n\t\t.capability-item.unsupported {\n\t\t\tborder-left-color: var(--wa-color-danger);\n\t\t\topacity: 0.6;\n\t\t}\n\t\t/* Success icon */\n\t\t.success-icon {\n\t\t\tfont-size: 4rem;\n\t\t\tcolor: var(--wa-color-success);\n\t\t}\n\t\t/* Verification code input styling */\n\t\t.verification-code-input {\n\t\t\ttext-align: center;\n\t\t\tfont-size: 1.5rem;\n\t\t\tletter-spacing: 0.3em;\n\t\t\tmax-width: 180px;\n\t\t}\n\t\t/* Radio group styling */\n\t\twa-radio-group[orientation=\"vertical\"] {\n\t\t\tdisplay: flex;\n\t\t\tflex-direction: column;\n\t\t\tgap: var(--wa-space-s);\n\t\t}\n\t\twa-radio[appearance=\"button\"] {\n\t\t\tcursor: pointer;\n\t\t}\n\t\twa-radio[appearance=\"button\"][disabled] {\n\t\t\tcursor: not-allowed;\n\t\t\topacity: 0.5;\n\t\t}\n\t\t/* Viewport constraints for popup/webview */\n\t\t@media (max-width: 400px) {\n\t\t\t.capability-item {\n\t\t\t\tpadding: var(--wa-space-xs) var(--wa-space-s);\n\t\t\t\tfont-size: var(--wa-font-size-s);\n\t\t\t}\n\t\t\t.verification-code-input {\n\t\t\t\tfont-size: 1.25rem;\n\t\t\t\tmax-width: 160px;\n\t\t\t}\n\t\t\t.success-icon {\n\t\t\t\tfont-size: 3rem;\n\t\t\t}\n\t\t}\n\t\t@media (max-height: 600px) {\n\t\t\t.capability-item {\n\t\t\t\tpadding: var(--wa-space-xs) var(--wa-space-s);\n\t\t\t}\n\t\t}\n\t</style>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// registerStep1Scripts handles method selection and continue button enabling
|
||||
func registerStep1Scripts() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<script>\n\t\t(function() {\n\t\t\tconst methodGroup = document.getElementById('auth-method-group');\n\t\t\tconst btnContinue = document.getElementById('btn-continue');\n\t\t\t\n\t\t\tif (methodGroup && btnContinue) {\n\t\t\t\t// Listen for method selection changes\n\t\t\t\tmethodGroup.addEventListener('wa-change', (e) => {\n\t\t\t\t\tconst selectedMethod = e.target.value;\n\t\t\t\t\tif (selectedMethod) {\n\t\t\t\t\t\tbtnContinue.removeAttribute('disabled');\n\t\t\t\t\t\t// Update the hx-get URL with selected method\n\t\t\t\t\t\tbtnContinue.setAttribute('hx-get', '/register/step/2?method=' + selectedMethod);\n\t\t\t\t\t\thtmx.process(btnContinue);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t})();\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// passkeyRegistrationScript handles WebAuthn passkey registration
|
||||
func passkeyRegistrationScript() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var23 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var23 == nil {
|
||||
templ_7745c5c3_Var23 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<script>\n\t\t(function() {\n\t\t\tconst btn = document.getElementById('btn-register-passkey');\n\t\t\tif (!btn) return;\n\t\t\t\n\t\t\tbtn.addEventListener('click', async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst username = document.querySelector('[name=\"username\"]')?.value;\n\t\t\t\t\tconst displayName = document.querySelector('[name=\"display-name\"]')?.value || username;\n\t\t\t\t\t\n\t\t\t\t\tif (!username) {\n\t\t\t\t\t\talert('Please enter a username');\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\t// Get challenge from server (in production)\n\t\t\t\t\tconst challenge = new Uint8Array(32);\n\t\t\t\t\tcrypto.getRandomValues(challenge);\n\t\t\t\t\t\n\t\t\t\t\tconst createOptions = {\n\t\t\t\t\t\tpublicKey: {\n\t\t\t\t\t\t\tchallenge: challenge,\n\t\t\t\t\t\t\trp: {\n\t\t\t\t\t\t\t\tname: \"Sonr\",\n\t\t\t\t\t\t\t\tid: window.location.hostname\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tuser: {\n\t\t\t\t\t\t\t\tid: new Uint8Array(16),\n\t\t\t\t\t\t\t\tname: username,\n\t\t\t\t\t\t\t\tdisplayName: displayName\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tpubKeyCredParams: [\n\t\t\t\t\t\t\t\t{ type: \"public-key\", alg: -7 }, // ES256\n\t\t\t\t\t\t\t\t{ type: \"public-key\", alg: -257 } // RS256\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tauthenticatorSelection: {\n\t\t\t\t\t\t\t\tauthenticatorAttachment: \"platform\",\n\t\t\t\t\t\t\t\tuserVerification: \"required\",\n\t\t\t\t\t\t\t\tresidentKey: \"required\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\ttimeout: 60000\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t\n\t\t\t\t\tconst credential = await navigator.credentials.create(createOptions);\n\t\t\t\t\tconsole.log(\"Passkey created:\", credential);\n\t\t\t\t\t\n\t\t\t\t\t// Navigate to success step\n\t\t\t\t\thtmx.ajax('GET', '/register/step/3', {\n\t\t\t\t\t\ttarget: '#step-content',\n\t\t\t\t\t\tswap: 'innerHTML transition:true'\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Passkey registration failed:\", error);\n\t\t\t\t\talert(\"Registration failed: \" + error.message);\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// securityKeyRegistrationScript handles WebAuthn security key registration
|
||||
func securityKeyRegistrationScript() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var24 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var24 == nil {
|
||||
templ_7745c5c3_Var24 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<script>\n\t\t(function() {\n\t\t\tconst btn = document.getElementById('btn-register-security-key');\n\t\t\tif (!btn) return;\n\t\t\t\n\t\t\tbtn.addEventListener('click', async () => {\n\t\t\t\ttry {\n\t\t\t\t\tconst username = document.querySelector('[name=\"username\"]')?.value;\n\t\t\t\t\t\n\t\t\t\t\tif (!username) {\n\t\t\t\t\t\talert('Please enter a username');\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\tconst challenge = new Uint8Array(32);\n\t\t\t\t\tcrypto.getRandomValues(challenge);\n\t\t\t\t\t\n\t\t\t\t\tconst createOptions = {\n\t\t\t\t\t\tpublicKey: {\n\t\t\t\t\t\t\tchallenge: challenge,\n\t\t\t\t\t\t\trp: {\n\t\t\t\t\t\t\t\tname: \"Sonr\",\n\t\t\t\t\t\t\t\tid: window.location.hostname\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tuser: {\n\t\t\t\t\t\t\t\tid: new Uint8Array(16),\n\t\t\t\t\t\t\t\tname: username,\n\t\t\t\t\t\t\t\tdisplayName: username\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tpubKeyCredParams: [\n\t\t\t\t\t\t\t\t{ type: \"public-key\", alg: -7 },\n\t\t\t\t\t\t\t\t{ type: \"public-key\", alg: -257 }\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tauthenticatorSelection: {\n\t\t\t\t\t\t\t\tauthenticatorAttachment: \"cross-platform\",\n\t\t\t\t\t\t\t\tuserVerification: \"preferred\"\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\ttimeout: 60000\n\t\t\t\t\t\t}\n\t\t\t\t\t};\n\t\t\t\t\t\n\t\t\t\t\tconst credential = await navigator.credentials.create(createOptions);\n\t\t\t\t\tconsole.log(\"Security key registered:\", credential);\n\t\t\t\t\t\n\t\t\t\t\t// Navigate to success step\n\t\t\t\t\thtmx.ajax('GET', '/register/step/3', {\n\t\t\t\t\t\ttarget: '#step-content',\n\t\t\t\t\t\tswap: 'innerHTML transition:true'\n\t\t\t\t\t});\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconsole.error(\"Security key registration failed:\", error);\n\t\t\t\t\talert(\"Registration failed: \" + error.message);\n\t\t\t\t}\n\t\t\t});\n\t\t})();\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// step3Scripts handles the confirmation checkbox and finish button
|
||||
func step3Scripts() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var25 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var25 == nil {
|
||||
templ_7745c5c3_Var25 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<script>\n\t\t(function() {\n\t\t\tconst confirmCheckbox = document.getElementById('confirm-saved');\n\t\t\tconst finishBtn = document.getElementById('btn-finish');\n\t\t\t\n\t\t\tif (confirmCheckbox && finishBtn) {\n\t\t\t\tconfirmCheckbox.addEventListener('wa-change', (e) => {\n\t\t\t\t\tif (e.target.checked) {\n\t\t\t\t\t\tfinishBtn.removeAttribute('disabled');\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfinishBtn.setAttribute('disabled', '');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\t\t})();\n\t</script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
Reference in New Issue
Block a user