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:
parent
c7af216ce3
commit
e802027236
23 changed files with 1042 additions and 121 deletions
43
frontend-reactive/app/components/auth.ts
Normal file
43
frontend-reactive/app/components/auth.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
18
frontend-reactive/app/components/auth/registration-form.hbs
Normal file
18
frontend-reactive/app/components/auth/registration-form.hbs
Normal 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>
|
21
frontend-reactive/app/components/auth/registration-form.ts
Normal file
21
frontend-reactive/app/components/auth/registration-form.ts
Normal 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 }> = []
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
30
frontend-reactive/app/components/util/string-array.hbs
Normal file
30
frontend-reactive/app/components/util/string-array.hbs
Normal 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>
|
57
frontend-reactive/app/components/util/string-array.ts
Normal file
57
frontend-reactive/app/components/util/string-array.ts
Normal 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
|
||||
}
|
||||
}
|
|
@ -8,4 +8,5 @@ export default class Router extends EmberRouter {
|
|||
|
||||
Router.map(function () {
|
||||
this.route('about');
|
||||
this.route('registerform');
|
||||
});
|
||||
|
|
9
frontend-reactive/app/routes/registerform.ts
Normal file
9
frontend-reactive/app/routes/registerform.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Route from '@ember/routing/route'
|
||||
|
||||
export default class RegisterFormRoute extends Route {
|
||||
async model() {
|
||||
return {
|
||||
list: [{ value: 'one' }, { value: 'two' }],
|
||||
}
|
||||
}
|
||||
}
|
122
frontend-reactive/app/services/auth.ts
Normal file
122
frontend-reactive/app/services/auth.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import Service from '@ember/service';
|
||||
import {
|
||||
startAuthentication,
|
||||
startRegistration,
|
||||
} from '@simplewebauthn/browser';
|
||||
|
||||
export default class AuthService extends Service {
|
||||
async startLogin(username: string) {
|
||||
// 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: 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) {
|
||||
return;
|
||||
} else {
|
||||
throw new Error(
|
||||
'Bad response code: ' +
|
||||
verificationResponse.status +
|
||||
'. Content: ' +
|
||||
msg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async startRegistration(username: string) {
|
||||
// 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: 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) {
|
||||
return;
|
||||
} else {
|
||||
throw new Error(
|
||||
'Bad response code: ' +
|
||||
verificationResponse.status +
|
||||
'. Content: ' +
|
||||
msg,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the client is currently logged in
|
||||
// Happens via caling an endpoint that's only available if logged in but doesn't return anything other than "ok"
|
||||
async getLoginState(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch('/api/test-auth');
|
||||
return response.status >= 200 && response.status < 300;
|
||||
} catch (error: any) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't remove this declaration: this is what enables TypeScript to resolve
|
||||
// this service using `Owner.lookup('service:auth')`, as well
|
||||
// as to check when you pass the service name as an argument to the decorator,
|
||||
// like `@service('auth') declare altName: AuthService;`.
|
||||
declare module '@ember/service' {
|
||||
interface Registry {
|
||||
auth: AuthService;
|
||||
}
|
||||
}
|
|
@ -8,3 +8,4 @@
|
|||
@import url("svgs.css");
|
||||
@import url("timeline.css");
|
||||
@import url("auth.css");
|
||||
@import url("stringArray.css");
|
||||
|
|
4
frontend-reactive/app/styles/stringArray.css
Normal file
4
frontend-reactive/app/styles/stringArray.css
Normal file
|
@ -0,0 +1,4 @@
|
|||
.string-array-element-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{{page-title "FrontendReactive"}}
|
||||
|
||||
{{outlet}}
|
||||
<Timeline @notes={{@model.notes}} />
|
||||
{{outlet}}{{!----}}
|
||||
{{!--<Timeline @notes={{@model.notes}} />--}}
|
2
frontend-reactive/app/templates/registerform.hbs
Normal file
2
frontend-reactive/app/templates/registerform.hbs
Normal file
|
@ -0,0 +1,2 @@
|
|||
{{page-title "RegisterForm"}}
|
||||
<Auth::RegistrationForm @username="bob" />
|
Loading…
Add table
Add a link
Reference in a new issue