feat: add passkey component

This commit is contained in:
Prad Nukala 2024-12-05 19:26:09 -05:00
parent d8b4ee852a
commit b6b4c38dfc
12 changed files with 118 additions and 309 deletions

12
Taskfile.yml Normal file
View File

@ -0,0 +1,12 @@
# https://taskfile.dev
version: '3'
vars:
GREETING: Hello, World!
tasks:
templ:
cmds:
- templ generate
silent: true

View File

@ -0,0 +1,22 @@
---
meta:
title: Passkey
description:
layout: component
---
```html:preview
<sl-passkey></sl-passkey>
```
## Examples
### First Example
TODO
### Second Example
TODO
[component-metadata:sl-passkey]

View File

@ -0,0 +1,43 @@
import { property } from 'lit/decorators.js';
import { html } from 'lit';
import { LocalizeController } from '../../utilities/localize.js';
import { watch } from '../../internal/watch.js';
import componentStyles from '../../styles/component.styles.js';
import ShoelaceElement from '../../internal/shoelace-element.js';
import styles from './passkey.styles.js';
import type { CSSResultGroup } from 'lit';
/**
* @summary Short summary of the component's intended use.
* @documentation https://shoelace.style/components/passkey
* @status experimental
* @since 2.0
*
* @dependency sl-example
*
* @event sl-event-name - Emitted as an example.
*
* @slot - The default slot.
* @slot example - An example slot.
*
* @csspart base - The component's base wrapper.
*
* @cssproperty --example - An example CSS custom property.
*/
export default class SlPasskey extends ShoelaceElement {
static styles: CSSResultGroup = [componentStyles, styles];
private readonly localize = new LocalizeController(this);
/** An example attribute. */
@property() attr = 'example';
@watch('example')
handleExampleChange() {
// do something
}
render() {
return html` <slot></slot> `;
}
}

View File

@ -0,0 +1,7 @@
import { css } from 'lit';
export default css`
:host {
display: block;
}
`;

View File

@ -0,0 +1,10 @@
import '../../../dist/shoelace.js';
import { expect, fixture, html } from '@open-wc/testing';
describe('<sl-passkey>', () => {
it('should render a component', async () => {
const el = await fixture(html` <sl-passkey></sl-passkey> `);
expect(el).to.exist;
});
});

View File

@ -0,0 +1,12 @@
import SlPasskey from './passkey.component.js';
export * from './passkey.component.js';
export default SlPasskey;
SlPasskey.define('sl-passkey');
declare global {
interface HTMLElementTagNameMap {
'sl-passkey': SlPasskey;
}
}

View File

@ -57,6 +57,7 @@ export { default as SlTooltip } from './components/tooltip/tooltip.js';
export { default as SlTree } from './components/tree/tree.js';
export { default as SlTreeItem } from './components/tree-item/tree-item.js';
export { default as SlVisuallyHidden } from './components/visually-hidden/visually-hidden.js';
export { default as SlPasskey } from './components/passkey/passkey.js';
/* plop:component */
// Utilities

View File

@ -1,15 +0,0 @@
package button
templ Primary(href string, text string) {
<div>
<div class="btn cursor-pointer text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow" hx-swap="afterend" hx-get={ href }>
{ text }
</div>
</div>
}
templ Secondary(href string, text string) {
<div>
<div x-on:click="toast('Default Toast Notification', 'default', '', 'top-center')" class="btn cursor-pointer text-zinc-600 bg-white hover:text-zinc-900 w-full shadow">{ text }</div>
</div>
}

View File

@ -1,108 +0,0 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.793
package button
//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"
func Primary(href string, text string) 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_Err = templ_7745c5c3_Buffer.WriteString("<div><div class=\"btn cursor-pointer text-zinc-100 bg-zinc-900 hover:bg-zinc-800 w-full shadow\" hx-swap=\"afterend\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(href)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/button/default.templ`, Line: 5, Col: 124}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/button/default.templ`, Line: 6, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
func Secondary(href string, text string) 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 = templ_7745c5c3_Buffer.WriteString("<div><div x-on:click=\"toast(&#39;Default Toast Notification&#39;, &#39;default&#39;, &#39;&#39;, &#39;top-center&#39;)\" class=\"btn cursor-pointer text-zinc-600 bg-white hover:text-zinc-900 w-full shadow\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(text)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/button/default.templ`, Line: 13, Col: 175}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return templ_7745c5c3_Err
})
}
var _ = templruntime.GeneratedTemplate

View File

@ -1,175 +0,0 @@
package form
import "github.com/go-webauthn/webauthn/protocol"
var credentialsHandle = templ.NewOnceHandle()
// Base credentials script template
templ CredentialsScripts() {
@credentialsHandle.Once() {
<script type="text/javascript">
// Check if WebAuthn is supported
async function isWebAuthnSupported() {
return window.PublicKeyCredential !== undefined;
}
// Create credentials
async function createCredential(options) {
try {
const publicKey = {
challenge: base64URLDecode(options.challenge),
rp: {
name: options.rpName,
id: options.rpId,
},
user: {
id: base64URLDecode(options.userId),
name: options.userName,
displayName: options.userDisplayName,
},
pubKeyCredParams: [{alg: -7, type: "public-key"}],
timeout: options.timeout || 60000,
attestation: options.attestationType || "none",
};
const credential = await navigator.credentials.create({
publicKey: publicKey
});
return {
id: credential.id,
rawId: arrayBufferToBase64URL(credential.rawId),
type: credential.type,
response: {
attestationObject: arrayBufferToBase64URL(credential.response.attestationObject),
clientDataJSON: arrayBufferToBase64URL(credential.response.clientDataJSON),
}
};
} catch (err) {
console.error('Error creating credential:', err);
throw err;
}
}
// Get credentials
async function getCredential(options) {
try {
const publicKey = {
challenge: base64URLDecode(options.challenge),
rpId: options.rpId,
timeout: options.timeout || 60000,
userVerification: options.userVerification || "preferred",
};
if (options.allowCredentials) {
publicKey.allowCredentials = options.allowCredentials.map(cred => ({
type: cred.type,
id: base64URLDecode(cred.id),
}));
}
const assertion = await navigator.credentials.get({
publicKey: publicKey
});
return {
id: assertion.id,
rawId: arrayBufferToBase64URL(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: arrayBufferToBase64URL(assertion.response.authenticatorData),
clientDataJSON: arrayBufferToBase64URL(assertion.response.clientDataJSON),
signature: arrayBufferToBase64URL(assertion.response.signature),
userHandle: assertion.response.userHandle ? arrayBufferToBase64URL(assertion.response.userHandle) : null
}
};
} catch (err) {
console.error('Error getting credential:', err);
throw err;
}
}
// Utility functions for base64URL encoding/decoding
function base64URLDecode(base64url) {
const padding = '='.repeat((4 - base64url.length % 4) % 4);
const base64 = (base64url + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const array = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; i++) {
array[i] = rawData.charCodeAt(i);
}
return array.buffer;
}
function arrayBufferToBase64URL(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
const base64 = window.btoa(binary);
return base64
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
</script>
}
}
script CreatePasskey(id string) {
function createPasskey(id) {
const passkey = document.getElementById(id);
passkey.value = window.crypto.getRandomValues(new Uint8Array(32)).join('');
}
}
// Template for creating credentials
templ CreateCredential(options *protocol.PublicKeyCredentialCreationOptions) {
@CredentialsScripts()
<script>
(async () => {
try {
if (!await isWebAuthnSupported()) {
throw new Error("WebAuthn is not supported in this browser");
}
const options = { templ.JSONString(options) };
const credential = await createCredential(options);
// Dispatch event with credential data
window.dispatchEvent(new CustomEvent('credentialCreated', {
detail: credential
}));
} catch (err) {
window.dispatchEvent(new CustomEvent('credentialError', {
detail: err.message
}));
}
})();
</script>
}
// Template for getting credentials
templ GetCredential(options *protocol.PublicKeyCredentialRequestOptions) {
@CredentialsScripts()
<script>
(async () => {
try {
if (!await isWebAuthnSupported()) {
throw new Error("WebAuthn is not supported in this browser");
}
const options = { templ.JSONString(options) };
const credential = await getCredential(options);
// Dispatch event with credential data
window.dispatchEvent(new CustomEvent('credentialRetrieved', {
detail: credential
}));
} catch (err) {
window.dispatchEvent(new CustomEvent('credentialError', {
detail: err.message
}));
}
})();
</script>
}

View File

@ -160,7 +160,7 @@ func Alpine() templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("alpinejs", "3.14.6", "dist/cdn.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 36, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 36, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@ -173,7 +173,7 @@ func Alpine() templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("@alpinejs/focus", "3.14.6", "dist/cdn.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 37, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 37, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@ -234,7 +234,7 @@ func Dexie() templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("dexie", "4.0.10", "dist/dexie.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 44, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 44, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@ -247,7 +247,7 @@ func Dexie() templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("dexie-export-import", "4.1.4", "dist/dexie-export-import.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 45, Col: 94}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 45, Col: 94}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
@ -308,7 +308,7 @@ func Htmx() templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("htmx.org", "1.9.12", "dist/htmx.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 52, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 52, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@ -321,7 +321,7 @@ func Htmx() templ.Component {
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("htmx-ext-include-vals", "2.0.0", "include-vals.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 53, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 53, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
if templ_7745c5c3_Err != nil {
@ -334,7 +334,7 @@ func Htmx() templ.Component {
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("htmx-ext-path-params", "2.0.0", "path-params.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 54, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 54, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil {
@ -347,7 +347,7 @@ func Htmx() templ.Component {
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("htmx-ext-alpine-morph", "2.0.0", "alpine-morph.min.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 55, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 55, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
@ -396,7 +396,7 @@ func Nebula(version string) templ.Component {
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("@onsonr/nebula", version, "cdn/themes/light.css"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 64, Col: 71}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 64, Col: 71}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
@ -409,7 +409,7 @@ func Nebula(version string) templ.Component {
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("@onsonr/nebula", version, "cdn/themes/dark.css"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 69, Col: 70}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 69, Col: 70}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
@ -438,7 +438,7 @@ func Nebula(version string) templ.Component {
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(jsDelivrURL("@onsonr/nebula", version, "cdn/shoelace-autoloader.js"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/scripts.templ`, Line: 73, Col: 98}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `ui/layout/imports.templ`, Line: 73, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil {