I FUCKING DID IT

Added a helper component for managing a list of strings.
This component could, in theory, also be turned into a generic one
for any type of data
This commit is contained in:
Melody Becker 2024-10-24 16:15:08 +02:00
parent c7af216ce3
commit e802027236
23 changed files with 1042 additions and 121 deletions

View file

@ -0,0 +1,43 @@
import { action } from '@ember/object';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import '@simplewebauthn/browser';
import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
import type AuthService from 'frontend-reactive/services/auth';
export interface PasskeySignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class Auth extends Component<PasskeySignature> {
@tracked username: string = '';
@tracked error: string | undefined;
@service declare auth: AuthService;
@action async startLogin() {
try {
this.auth.startLogin(this.username);
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
@action async startRegistration() {
try {
this.auth.startRegistration(this.username);
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
}

View file

@ -0,0 +1,18 @@
<div class="registration-form">
<p class="registration-form-username">{{this.username}}</p>
<label>
Displayname
<Input @type="text" @value={{this.displayname}} placeholder="Displayname" />
</label>
<label>
Description
<Input
@type="text"
@value={{this.description}}
placeholder="Account description"
/>
</label>
<Util::StringArray @list={{this.gender}} />
<p>{{this.extracted}}</p>
</div>

View file

@ -0,0 +1,21 @@
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
export interface AuthRegistrationFormSignature {
// The arguments accepted by the component
Args: {
username: string
}
// Any blocks yielded by the component
Blocks: {
default: []
}
// The element to which `...attributes` is applied in the component template
Element: null
}
export default class AuthRegistrationForm extends Component<AuthRegistrationFormSignature> {
@tracked displayname: string = this.args.username
@tracked description: string = ''
@tracked gender: Array<{ value: string }> = []
}

View file

@ -1,119 +0,0 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import '@simplewebauthn/browser';
import {
startAuthentication,
startRegistration,
} from '@simplewebauthn/browser';
export interface PasskeySignature {
// The arguments accepted by the component
Args: {};
// Any blocks yielded by the component
Blocks: {
default: [];
};
// The element to which `...attributes` is applied in the component template
Element: null;
}
export default class Passkey extends Component<PasskeySignature> {
@tracked username: string = '';
@tracked error: string | undefined;
@action async startLogin() {
try {
// Get login options from your server. Here, we also receive the challenge.
const response = await fetch('/webauthn/passkey/loginBegin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: this.username }),
});
// Check if the login options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error('Failed to get login options from server: ' + msg);
}
// Convert the login options to JSON.
const options = await response.json();
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new assertionResponse is created. This also means that the challenge has been signed.
const assertionResponse = await startAuthentication(options.publicKey);
// Send assertionResponse back to server for verification.
const verificationResponse = await fetch(
'/webauthn/passkey/loginFinish',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(assertionResponse),
},
);
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
this.error = undefined;
} else {
this.error = msg;
}
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
@action async startRegistration() {
try {
// Get registration options from your server. Here, we also receive the challenge.
const response = await fetch('/webauthn/passkey/registerBegin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: this.username }),
});
// Check if the registration options are ok.
if (!response.ok) {
const msg = await response.json();
throw new Error(
'User already exists or failed to get registration options from server: ' +
msg,
);
}
// Convert the registration options to JSON.
const options = await response.json();
console.log('registration start', options);
// This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello).
// A new attestation is created. This also means a new public-private-key pair is created.
const attestationResponse = await startRegistration(options.publicKey);
console.log('Attempting to complete registration', attestationResponse);
// Send attestationResponse back to server for verification and storage.
const verificationResponse = await fetch(
'/webauthn/passkey/registerFinish',
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(attestationResponse),
},
);
const msg = await verificationResponse.json();
if (verificationResponse.ok) {
this.error = undefined;
} else {
this.error = msg;
}
} catch (error: any) {
this.error = 'Error: ' + error.message;
}
}
}

View file

@ -0,0 +1,30 @@
<div class="{{@wrapper-classes}}">
<ul>
{{#each this.args.list as |element index|}}
<li>
<div class="string-array-element-wrapper">
<Input
@type="text"
@value={{element.value}}
/>
<div
class="{{@remove-element-classes}}"
type="button"
id="{{this.args.prefix}}-{{index}}"
{{on "click" this.removeElement}}
>
X
</div>
</div>
</li>
{{/each}}
</ul>
<div
class="{{@add-element-classes}}"
type="button"
{{on "click" this.addElement}}
>
Add element
</div>
</div>

View file

@ -0,0 +1,57 @@
import MutableArray from '@ember/array/mutable'
import { action } from '@ember/object'
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
export interface UtilStringArraySignature {
// The arguments accepted by the component
Args: {
list: MutableArray<{ value: string }>
prefix: string
}
// Any blocks yielded by the component
Blocks: {
default: []
}
// The element to which `...attributes` is applied in the component template
Element: null
}
export default class UtilStringArray extends Component<UtilStringArraySignature> {
@action addElement() {
MutableArray.apply(this.args.list)
this.args.list.pushObject({ value: '' })
}
@action removeElement(event: MouseEvent) {
MutableArray.apply(this.args.list)
const target = event.target as HTMLDivElement
const splits = target.id.split('-', 2)
if (splits.length != 2) return
const indexStr = splits[1]
//console.log('Content: ', indexStr)
if (!indexStr) return
//let index = this.args.list.find((elem) => elem == content)
//let index = this.listCopy.findIndex((d) => d == content)
this.args.list.removeAt(Number(indexStr))
}
transformArrayIntoUsable(arr: Array<string>): { [key: number]: string } {
const out: { [key: number]: string } = {}
const tmp = arr.map((elem: string, index: number) => {
out[index] = elem
return elem
})
return out
}
countElemsInObj(obj: any): number {
let count = 0
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) ++count
}
return count
}
}