Add JRPC API, reworked Login and User pages
This commit is contained in:
@ -1,124 +1,34 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {} from "flowbite-svelte";
|
||||
|
||||
import { LoginState } from "@hibas123/openauth-internalapi";
|
||||
import Theme from "../../components/theme";
|
||||
import loginState from "./state";
|
||||
import HoveringContentBox from "../../components/HoveringContentBox.svelte";
|
||||
import Api from "./api.ts";
|
||||
import Credentials from "./Credentials.svelte";
|
||||
import Redirect from "./Redirect.svelte";
|
||||
import Twofactor from "./Twofactor.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import Username from "./Username.svelte";
|
||||
import Password from "./Password.svelte";
|
||||
import Success from "./Success.svelte";
|
||||
import TwoFactor from "./TwoFactor.svelte";
|
||||
|
||||
const appname = "OpenAuth";
|
||||
|
||||
const states = {
|
||||
credentials: 1,
|
||||
twofactor: 3,
|
||||
redirect: 4,
|
||||
};
|
||||
|
||||
let username = Api.getUsername();
|
||||
let password = "";
|
||||
|
||||
let loading = false;
|
||||
let state = states.credentials;
|
||||
|
||||
function getButtonText(state) {
|
||||
switch (state) {
|
||||
case states.username:
|
||||
return "Next";
|
||||
case states.password:
|
||||
return "Login";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
$: btnText = getButtonText(state);
|
||||
|
||||
let error;
|
||||
|
||||
// window.addEventListener("popstate", () => {
|
||||
// state = history.state;
|
||||
// })
|
||||
|
||||
function LoadRedirect() {
|
||||
state = states.redirect;
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
state = states.loading;
|
||||
}
|
||||
|
||||
let salt;
|
||||
async function buttonClick() {
|
||||
if (state === states.username) {
|
||||
Loading();
|
||||
let res = await Api.setUsername(username);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
LoadUsername();
|
||||
} else {
|
||||
LoadPassword();
|
||||
}
|
||||
} else if (state === states.password) {
|
||||
Loading();
|
||||
let res = await Api.setPassword(password);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
LoadPassword();
|
||||
} else {
|
||||
if (res.tfa) {
|
||||
// TODO: Make TwoFactor UI/-s
|
||||
} else {
|
||||
LoadRedirect();
|
||||
}
|
||||
}
|
||||
btnText = "Error";
|
||||
}
|
||||
}
|
||||
|
||||
function startRedirect() {
|
||||
state = states.redirect;
|
||||
// Show message to User and then redirect
|
||||
setTimeout(() => Api.finish(), 2000);
|
||||
}
|
||||
|
||||
function afterCredentials() {
|
||||
Object.keys(Api); // Some weird bug needs this???
|
||||
|
||||
if (Api.twofactor) {
|
||||
state = states.twofactor;
|
||||
} else {
|
||||
startRedirect();
|
||||
}
|
||||
}
|
||||
|
||||
function afterTwoFactor() {
|
||||
startRedirect();
|
||||
}
|
||||
const { state } = loginState;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Theme>
|
||||
<HoveringContentBox title="Login" {loading}>
|
||||
<HoveringContentBox title="Login" loading={$state.loading}>
|
||||
<form action="JavaScript:void(0)">
|
||||
{#if state === states.redirect}
|
||||
<Redirect />
|
||||
{:else if state === states.credentials}
|
||||
<Credentials next={afterCredentials} setLoading={(s) => (loading = s)} />
|
||||
{:else if state === states.twofactor}
|
||||
<Twofactor finish={afterTwoFactor} setLoading={(s) => (loading = s)} />
|
||||
{#if $state.success}
|
||||
<Success />
|
||||
{:else if !$state.username}
|
||||
<Username on:username={(evt) => loginState.setUsername(evt.detail)} />
|
||||
{:else if !$state.password}
|
||||
<Password
|
||||
username={$state.username}
|
||||
on:password={(evt) => loginState.setPassword(evt.detail)}
|
||||
/>
|
||||
{:else if $state.requireTwoFactor.length > 0}
|
||||
<TwoFactor />
|
||||
{/if}
|
||||
</form>
|
||||
</HoveringContentBox>
|
||||
<footer>
|
||||
<p>Powered by {appname}</p>
|
||||
</footer>
|
||||
</Theme>
|
||||
|
||||
@ -1,84 +0,0 @@
|
||||
<script>
|
||||
import Api from "./api.ts";
|
||||
|
||||
let error;
|
||||
let password = "";
|
||||
let username = Api.getUsername();
|
||||
|
||||
const states = {
|
||||
username: 1,
|
||||
password: 2
|
||||
};
|
||||
|
||||
let state = states.username;
|
||||
|
||||
let salt;
|
||||
|
||||
export let setLoading;
|
||||
export let next;
|
||||
|
||||
async function buttonClick() {
|
||||
setLoading(true);
|
||||
if (state === states.username) {
|
||||
let res = await Api.setUsername(username);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
} else {
|
||||
state = states.password;
|
||||
error = undefined;
|
||||
}
|
||||
} else if (state === states.password) {
|
||||
let res = await Api.setPassword(password);
|
||||
if (res.error) {
|
||||
error = res.error;
|
||||
} else {
|
||||
error = undefined;
|
||||
next();
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.wide-button {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
{#if state === states.username}
|
||||
<h3>Enter your Username or your E-Mail Address</h3>
|
||||
<div class="floating group">
|
||||
<input
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
bind:value={username} />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label>Username or E-Mail</label>
|
||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<h3>Enter password for {username}</h3>
|
||||
<div class="floating group">
|
||||
<input
|
||||
type="password"
|
||||
autocomplete="password"
|
||||
autofocus
|
||||
bind:value={password} />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label>Password</label>
|
||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-primary wide-button" on:click={buttonClick}>Next</button>
|
||||
16
Frontend/src/pages/login/Error.svelte
Normal file
16
Frontend/src/pages/login/Error.svelte
Normal file
@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import loginState from "./state";
|
||||
|
||||
let { state } = loginState;
|
||||
</script>
|
||||
|
||||
{#if $state.error}
|
||||
<div class="error">{$state.error}</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
padding: 4px;
|
||||
}
|
||||
</style>
|
||||
30
Frontend/src/pages/login/Password.svelte
Normal file
30
Frontend/src/pages/login/Password.svelte
Normal file
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Error from "./Error.svelte";
|
||||
|
||||
let password: string = "";
|
||||
export let username: string;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<h3>Enter the password for {username}</h3>
|
||||
<div class="floating group">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autocomplete="password"
|
||||
autofocus
|
||||
bind:value={password}
|
||||
/>
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="password">Password</label>
|
||||
<Error />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-wide"
|
||||
on:click={() => dispatch("password", password)}>Next</button
|
||||
>
|
||||
@ -1,8 +1,8 @@
|
||||
<script>
|
||||
import Cleave from "cleave.js";
|
||||
import { onMount } from "svelte";
|
||||
import Error from "../Error.svelte";
|
||||
|
||||
export let error;
|
||||
// export let label;
|
||||
export let value;
|
||||
export let length = 6;
|
||||
@ -17,17 +17,11 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="floating group">
|
||||
<input id="noasidhglk" bind:this={input} autofocus bind:value />
|
||||
<input id="code-input" bind:this={input} autofocus bind:value />
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="noasidhglk">Code</label>
|
||||
<div class="error" style={!error ? 'display: none;' : ''}>{error}</div>
|
||||
<label for="code-input">Code</label>
|
||||
|
||||
<Error />
|
||||
</div>
|
||||
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
21
Frontend/src/pages/login/TF/TOTP.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import Error from "../Error.svelte";
|
||||
import loginState from "../state";
|
||||
import CodeInput from "./CodeInput.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let name: string;
|
||||
|
||||
let code: string = "";
|
||||
|
||||
function send() {
|
||||
loginState.useTOTP(id, code);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3>TOTP {name}</h3>
|
||||
<CodeInput bind:value={code} length={6} />
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary btn-wide" on:click={send}> Send </button>
|
||||
</div>
|
||||
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
28
Frontend/src/pages/login/TF/WebAuthn.svelte
Normal file
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Error from "../Error.svelte";
|
||||
import loginState from "../state";
|
||||
import { startAuthentication } from "@simplewebauthn/browser";
|
||||
|
||||
export let id: string;
|
||||
|
||||
async function doAuth() {
|
||||
let challenge = await loginState.getWebAuthnChallenge(id);
|
||||
try {
|
||||
loginState.setLoading(true);
|
||||
let result = await startAuthentication(JSON.parse(challenge));
|
||||
await loginState.useWebAuthn(id, result);
|
||||
} catch (e) {
|
||||
loginState.setError(e.message);
|
||||
return;
|
||||
} finally {
|
||||
loginState.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
doAuth();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Error />
|
||||
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
114
Frontend/src/pages/login/TwoFactor.svelte
Normal file
@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import loginState from "./state";
|
||||
import Icon from "./icons/Icon.svelte";
|
||||
import { TFAType } from "@hibas123/openauth-internalapi";
|
||||
import { onMount } from "svelte";
|
||||
import Totp from "./TF/TOTP.svelte";
|
||||
import Error from "./Error.svelte";
|
||||
import WebAuthn from "./TF/WebAuthn.svelte";
|
||||
|
||||
let { state } = loginState;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let selected = undefined;
|
||||
|
||||
$: {
|
||||
if ($state.requireTwoFactor?.length == 1) {
|
||||
selected = $state.requireTwoFactor[0];
|
||||
}
|
||||
}
|
||||
|
||||
const typeIconMap = {
|
||||
[TFAType.TOTP]: "Authenticator",
|
||||
[TFAType.BACKUP_CODE]: "BackupCode",
|
||||
[TFAType.WEBAUTHN]: "SecurityKey",
|
||||
[TFAType.APP_ALLOW]: "AppPush",
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !selected}
|
||||
<h3>Choose your 2FA method</h3>
|
||||
<ul>
|
||||
{#each $state.requireTwoFactor ?? [] as method}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<li on:click={() => (selected = method)}>
|
||||
<div class="icon">
|
||||
<Icon icon_name={typeIconMap[method.tfatype]} />
|
||||
</div>
|
||||
|
||||
<div class="name">{method.name}</div>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
<Error />
|
||||
</ul>
|
||||
{:else}
|
||||
{#if selected.tfatype == TFAType.TOTP}
|
||||
<Totp id={selected.id} name={selected.name} />
|
||||
{:else if selected.tfatype == TFAType.BACKUP_CODE}
|
||||
backup
|
||||
{:else if selected.tfatype == TFAType.WEBAUTHN}
|
||||
<WebAuthn id={selected.id} />
|
||||
{:else if selected.tfatype == TFAType.APP_ALLOW}
|
||||
appallow
|
||||
{:else}
|
||||
<p>Unknown 2FA type</p>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
<a
|
||||
class="to-list"
|
||||
href="# "
|
||||
on:click={(evt) => {
|
||||
evt.preventDefault();
|
||||
loginState.setError(undefined);
|
||||
selected = undefined;
|
||||
}}
|
||||
>
|
||||
Choose another Method
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
border-top: 1px grey solid;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
li:hover {
|
||||
background-color: #e2e2e2;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 1rem;
|
||||
line-height: 1.5rem;
|
||||
font-size: 20px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.to-list {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
@ -1,104 +0,0 @@
|
||||
<script>
|
||||
import Api, { TFATypes } from "./api.ts";
|
||||
import Icon from "./icons/Icon.svelte";
|
||||
|
||||
import OTCTwoFactor from "./twofactors/otc.svelte";
|
||||
import PushTwoFactor from "./twofactors/push.svelte";
|
||||
import U2FTwoFactor from "./twofactors/u2f.svelte";
|
||||
|
||||
const states = {
|
||||
list: 1,
|
||||
twofactor: 2
|
||||
};
|
||||
|
||||
function getIcon(tf) {
|
||||
switch (tf.type) {
|
||||
case TFATypes.OTC:
|
||||
return "Authenticator";
|
||||
case TFATypes.BACKUP_CODE:
|
||||
return "BackupCode";
|
||||
case TFATypes.U2F:
|
||||
return "SecurityKey";
|
||||
case TFATypes.APP_ALLOW:
|
||||
return "AppPush";
|
||||
}
|
||||
}
|
||||
|
||||
let twofactors = Api.twofactor.map(tf => {
|
||||
return {
|
||||
...tf,
|
||||
icon: getIcon(tf)
|
||||
};
|
||||
});
|
||||
|
||||
let state = states.list;
|
||||
|
||||
let twofactor = undefined;
|
||||
twofactor = twofactors[0];
|
||||
$: console.log(twofactor);
|
||||
|
||||
function onFinish(res) {
|
||||
if (res) finish();
|
||||
else twofactor = undefined;
|
||||
}
|
||||
|
||||
export let finish;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
border-top: 1px grey solid;
|
||||
padding: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li:first-child {
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
float: left;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 48px;
|
||||
line-height: 24px;
|
||||
font-size: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
{#if !twofactor}
|
||||
<h3>Select your Authentication method:</h3>
|
||||
<ul>
|
||||
{#each twofactors as tf}
|
||||
<li on:click={() => (twofactor = tf)}>
|
||||
<div class="icon">
|
||||
<Icon icon_name={tf.icon} />
|
||||
</div>
|
||||
|
||||
<div class="name">{tf.name}</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else if twofactor.type === TFATypes.OTC}
|
||||
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={true} />
|
||||
{:else if twofactor.type === TFATypes.BACKUP_CODE}
|
||||
<OTCTwoFactor id={twofactor.id} finish={onFinish} otc={false} />
|
||||
{:else if twofactor.type === TFATypes.U2F}
|
||||
<U2FTwoFactor id={twofactor.id} finish={onFinish} />
|
||||
{:else if twofactor.type === TFATypes.APP_ALLOW}
|
||||
<PushTwoFactor id={twofactor.id} finish={onFinish} />
|
||||
{:else}
|
||||
<div>Invalid TwoFactor Method!</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
29
Frontend/src/pages/login/Username.svelte
Normal file
29
Frontend/src/pages/login/Username.svelte
Normal file
@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import Error from "./Error.svelte";
|
||||
|
||||
let username: string = "";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<h3>Enter your Username or your E-Mail Address</h3>
|
||||
<div class="floating group">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
autofocus
|
||||
bind:value={username}
|
||||
/>
|
||||
<span class="highlight" />
|
||||
<span class="bar" />
|
||||
<label for="username">Username or E-Mail</label>
|
||||
<Error />
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="btn btn-primary btn-wide"
|
||||
on:click={() => dispatch("username", username)}>Next</button
|
||||
>
|
||||
@ -1,182 +0,0 @@
|
||||
import request from "../../helper/request";
|
||||
import sha from "../../helper/sha512";
|
||||
import { setCookie, getCookie } from "../../helper/cookie";
|
||||
|
||||
export interface TwoFactor {
|
||||
id: string;
|
||||
name: string;
|
||||
type: TFATypes;
|
||||
}
|
||||
|
||||
export enum TFATypes {
|
||||
OTC,
|
||||
BACKUP_CODE,
|
||||
U2F,
|
||||
APP_ALLOW,
|
||||
}
|
||||
|
||||
// const Api = {
|
||||
// // twofactor: [{
|
||||
// // id: "1",
|
||||
// // name: "Backup Codes",
|
||||
// // type: TFATypes.BACKUP_CODE
|
||||
// // }, {
|
||||
// // id: "2",
|
||||
// // name: "YubiKey",
|
||||
// // type: TFATypes.U2F
|
||||
// // }, {
|
||||
// // id: "3",
|
||||
// // name: "Authenticator",
|
||||
// // type: TFATypes.OTC
|
||||
// // }] as TwoFactor[],
|
||||
|
||||
// }
|
||||
|
||||
export interface IToken {
|
||||
token: string;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
function makeid(length) {
|
||||
var result = "";
|
||||
var characters =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var charactersLength = characters.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export default class Api {
|
||||
static salt: string;
|
||||
static login: IToken;
|
||||
static special: IToken;
|
||||
static username: string;
|
||||
|
||||
static twofactor: any[];
|
||||
|
||||
static getUsername() {
|
||||
return this.username || getCookie("username");
|
||||
}
|
||||
|
||||
static async setUsername(
|
||||
username: string
|
||||
): Promise<{ error: string | undefined }> {
|
||||
return request(
|
||||
"/api/user/login",
|
||||
{
|
||||
type: "username",
|
||||
username,
|
||||
},
|
||||
"POST"
|
||||
)
|
||||
.then((res) => {
|
||||
this.salt = res.salt;
|
||||
this.username = username;
|
||||
return {
|
||||
error: undefined,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
let error = err.message;
|
||||
return { error };
|
||||
});
|
||||
}
|
||||
|
||||
static async setPassword(
|
||||
password: string
|
||||
): Promise<{ error: string | undefined; twofactor?: any }> {
|
||||
const date = new Date().valueOf();
|
||||
let pw = sha(sha(this.salt + password) + date.toString());
|
||||
return request(
|
||||
"/api/user/login",
|
||||
{
|
||||
type: "password",
|
||||
},
|
||||
"POST",
|
||||
{
|
||||
username: this.username,
|
||||
password: pw,
|
||||
date,
|
||||
}
|
||||
)
|
||||
.then(({ login, special, tfa }) => {
|
||||
this.login = login;
|
||||
this.special = special;
|
||||
|
||||
if (tfa && Array.isArray(tfa) && tfa.length > 0)
|
||||
this.twofactor = tfa;
|
||||
else this.twofactor = undefined;
|
||||
|
||||
return {
|
||||
error: undefined,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
let error = err.message;
|
||||
return { error };
|
||||
});
|
||||
}
|
||||
|
||||
static gettok() {
|
||||
return {
|
||||
login: this.login.token,
|
||||
special: this.special.token,
|
||||
};
|
||||
}
|
||||
|
||||
static async sendBackup(id: string, code: string) {
|
||||
return request("/api/user/twofactor/backup", this.gettok(), "PUT", {
|
||||
code,
|
||||
id,
|
||||
})
|
||||
.then(({ login_exp, special_exp }) => {
|
||||
this.login.expires = login_exp;
|
||||
this.special.expires = special_exp;
|
||||
return {};
|
||||
})
|
||||
.catch((err) => ({ error: err.message }));
|
||||
}
|
||||
|
||||
static async sendOTC(id: string, code: string) {
|
||||
return request("/api/user/twofactor/otc", this.gettok(), "PUT", {
|
||||
code,
|
||||
id,
|
||||
})
|
||||
.then(({ login_exp, special_exp }) => {
|
||||
this.login.expires = login_exp;
|
||||
this.special.expires = special_exp;
|
||||
return {};
|
||||
})
|
||||
.catch((error) => ({ error: error.message }));
|
||||
}
|
||||
|
||||
static finish() {
|
||||
let d = new Date();
|
||||
d.setTime(d.getTime() + 30 * 24 * 60 * 60 * 1000); //Keep the username 30 days
|
||||
setCookie("username", this.username, d.toUTCString());
|
||||
|
||||
setCookie(
|
||||
"login",
|
||||
this.login.token,
|
||||
new Date(this.login.expires).toUTCString()
|
||||
);
|
||||
setCookie(
|
||||
"special",
|
||||
this.special.token,
|
||||
new Date(this.special.expires).toUTCString()
|
||||
);
|
||||
|
||||
let url = new URL(window.location.href);
|
||||
let state = url.searchParams.get("state");
|
||||
let red = "/";
|
||||
|
||||
if (state) {
|
||||
let base64 = url.searchParams.get("base64");
|
||||
if (base64) red = atob(state);
|
||||
else red = state;
|
||||
}
|
||||
setTimeout(() => (window.location.href = red), 200);
|
||||
}
|
||||
}
|
||||
183
Frontend/src/pages/login/state.ts
Normal file
183
Frontend/src/pages/login/state.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import type { LoginState } from "@hibas123/openauth-internalapi";
|
||||
import { derived, get, writable } from "svelte/store";
|
||||
import InternalAPI from "../../helper/api";
|
||||
import sha from "../../helper/sha512";
|
||||
|
||||
interface LocalLoginState extends LoginState {
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
class LoginStore {
|
||||
state = writable<LocalLoginState>({
|
||||
username: undefined,
|
||||
password: false,
|
||||
passwordSalt: undefined,
|
||||
requireTwoFactor: [],
|
||||
success: false,
|
||||
loading: true,
|
||||
error: undefined
|
||||
})
|
||||
|
||||
isFinished = derived(this.state, $state => $state.success);
|
||||
|
||||
constructor() {
|
||||
this.state.subscribe((state) => {
|
||||
if (state.success) {
|
||||
setTimeout(() => {
|
||||
this.finish();
|
||||
}, 2000);
|
||||
}
|
||||
})
|
||||
this.getState();
|
||||
}
|
||||
|
||||
setLoading(loading: boolean) {
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
loading,
|
||||
error: loading ? undefined : current.error,
|
||||
}));
|
||||
}
|
||||
|
||||
setError(error: string) {
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
error,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
async getState() {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.GetState();
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async setUsername(username: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.Start(username);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
username
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async setPassword(password: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
const date = new Date().valueOf();
|
||||
let salt = get(this.state).passwordSalt
|
||||
let pw = sha(sha(salt + password) + date.toString());
|
||||
|
||||
let state = await InternalAPI.Login.UsePassword(pw, date);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useTOTP(id: string, code: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseTOTP(id, code);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useBackupCode(id: string, code: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseBackupCode(id, code);
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async getWebAuthnChallenge(id: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let challenge = await InternalAPI.Login.GetWebAuthnChallenge(id);
|
||||
return challenge;
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async useWebAuthn(id: string, response: any) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
|
||||
let state = await InternalAPI.Login.UseWebAuthn(id, JSON.stringify(response));
|
||||
this.state.update(current => ({
|
||||
...current,
|
||||
...state,
|
||||
}));
|
||||
} catch (err) {
|
||||
this.setError(err.message);
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async finish() {
|
||||
let url = new URL(window.location.href);
|
||||
let state = url.searchParams.get("state");
|
||||
let red = "/";
|
||||
|
||||
if (state) {
|
||||
let base64 = url.searchParams.get("base64");
|
||||
if (base64) red = atob(state);
|
||||
else red = state;
|
||||
}
|
||||
setTimeout(() => (window.location.href = red), 200);
|
||||
}
|
||||
}
|
||||
|
||||
const loginState = new LoginStore();
|
||||
|
||||
export default loginState;
|
||||
@ -1,50 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
import Api from "../api.ts";
|
||||
import CodeInput from "./codeInput.svelte";
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let finish;
|
||||
export let id;
|
||||
|
||||
export let otc = false;
|
||||
let title = otc ? "One Time Code (OTC)" : "Backup Code";
|
||||
let length = otc ? 6 : 8;
|
||||
|
||||
async function sendCode() {
|
||||
let c = code.replace(/\s+/g, "");
|
||||
if (c.length < length) {
|
||||
error = `Code must be ${length} digits long!`;
|
||||
} else {
|
||||
error = "";
|
||||
let res;
|
||||
if (otc) res = await Api.sendOTC(id, c);
|
||||
else res = await Api.sendBackup(id, c);
|
||||
if (res.error) error = res.error;
|
||||
else finish(true);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-next {
|
||||
margin: 0;
|
||||
margin-left: auto;
|
||||
min-width: 80px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>{title}</h3>
|
||||
|
||||
<CodeInput bind:value={code} label="Code" {error} {length} />
|
||||
|
||||
<div class="actions">
|
||||
<ToList {finish} />
|
||||
<button class="btn btn-primary btn-next" on:click={sendCode}> Send </button>
|
||||
</div>
|
||||
@ -1,389 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let device = "Handy01";
|
||||
// export let deviceId = "";
|
||||
|
||||
export let finish;
|
||||
|
||||
async function requestPush() {
|
||||
// Push Request
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.windows8 {
|
||||
position: relative;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin: 2rem auto;
|
||||
}
|
||||
|
||||
.windows8 .wBall {
|
||||
position: absolute;
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
opacity: 0;
|
||||
transform: rotate(225deg);
|
||||
-o-transform: rotate(225deg);
|
||||
-ms-transform: rotate(225deg);
|
||||
-webkit-transform: rotate(225deg);
|
||||
-moz-transform: rotate(225deg);
|
||||
animation: orbit 5.7425s infinite;
|
||||
-o-animation: orbit 5.7425s infinite;
|
||||
-ms-animation: orbit 5.7425s infinite;
|
||||
-webkit-animation: orbit 5.7425s infinite;
|
||||
-moz-animation: orbit 5.7425s infinite;
|
||||
}
|
||||
|
||||
.windows8 .wBall .wInnerBall {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: rgb(0, 140, 255);
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.windows8 #wBall_1 {
|
||||
animation-delay: 1.256s;
|
||||
-o-animation-delay: 1.256s;
|
||||
-ms-animation-delay: 1.256s;
|
||||
-webkit-animation-delay: 1.256s;
|
||||
-moz-animation-delay: 1.256s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_2 {
|
||||
animation-delay: 0.243s;
|
||||
-o-animation-delay: 0.243s;
|
||||
-ms-animation-delay: 0.243s;
|
||||
-webkit-animation-delay: 0.243s;
|
||||
-moz-animation-delay: 0.243s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_3 {
|
||||
animation-delay: 0.5065s;
|
||||
-o-animation-delay: 0.5065s;
|
||||
-ms-animation-delay: 0.5065s;
|
||||
-webkit-animation-delay: 0.5065s;
|
||||
-moz-animation-delay: 0.5065s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_4 {
|
||||
animation-delay: 0.7495s;
|
||||
-o-animation-delay: 0.7495s;
|
||||
-ms-animation-delay: 0.7495s;
|
||||
-webkit-animation-delay: 0.7495s;
|
||||
-moz-animation-delay: 0.7495s;
|
||||
}
|
||||
|
||||
.windows8 #wBall_5 {
|
||||
animation-delay: 1.003s;
|
||||
-o-animation-delay: 1.003s;
|
||||
-ms-animation-delay: 1.003s;
|
||||
-webkit-animation-delay: 1.003s;
|
||||
-moz-animation-delay: 1.003s;
|
||||
}
|
||||
|
||||
@keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
transform: rotate(180deg);
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
transform: rotate(300deg);
|
||||
animation-timing-function: linear;
|
||||
origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: rotate(410deg);
|
||||
animation-timing-function: ease-in-out;
|
||||
origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
transform: rotate(645deg);
|
||||
animation-timing-function: linear;
|
||||
origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
transform: rotate(770deg);
|
||||
animation-timing-function: ease-out;
|
||||
origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
transform: rotate(900deg);
|
||||
animation-timing-function: ease-out;
|
||||
origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-o-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-o-transform: rotate(180deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(300deg);
|
||||
-o-animation-timing-function: linear;
|
||||
-o-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(410deg);
|
||||
-o-animation-timing-function: ease-in-out;
|
||||
-o-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(645deg);
|
||||
-o-animation-timing-function: linear;
|
||||
-o-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(770deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
-o-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-o-transform: rotate(900deg);
|
||||
-o-animation-timing-function: ease-out;
|
||||
-o-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-o-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-o-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-ms-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-ms-transform: rotate(180deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(300deg);
|
||||
-ms-animation-timing-function: linear;
|
||||
-ms-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(410deg);
|
||||
-ms-animation-timing-function: ease-in-out;
|
||||
-ms-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(645deg);
|
||||
-ms-animation-timing-function: linear;
|
||||
-ms-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(770deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
-ms-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-ms-transform: rotate(900deg);
|
||||
-ms-animation-timing-function: ease-out;
|
||||
-ms-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-ms-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-ms-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-webkit-transform: rotate(180deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(300deg);
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(410deg);
|
||||
-webkit-animation-timing-function: ease-in-out;
|
||||
-webkit-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(645deg);
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(770deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
-webkit-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-webkit-transform: rotate(900deg);
|
||||
-webkit-animation-timing-function: ease-out;
|
||||
-webkit-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes orbit {
|
||||
0% {
|
||||
opacity: 1;
|
||||
z-index: 99;
|
||||
-moz-transform: rotate(180deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
7% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(300deg);
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-origin: 0%;
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(410deg);
|
||||
-moz-animation-timing-function: ease-in-out;
|
||||
-moz-origin: 7%;
|
||||
}
|
||||
|
||||
39% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(645deg);
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-origin: 30%;
|
||||
}
|
||||
|
||||
70% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(770deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
-moz-origin: 39%;
|
||||
}
|
||||
|
||||
75% {
|
||||
opacity: 1;
|
||||
-moz-transform: rotate(900deg);
|
||||
-moz-animation-timing-function: ease-out;
|
||||
-moz-origin: 70%;
|
||||
}
|
||||
|
||||
76% {
|
||||
opacity: 0;
|
||||
-moz-transform: rotate(900deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-moz-transform: rotate(900deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>SMS</h3>
|
||||
|
||||
<p>A code was sent to your Device <b>{device}</b></p>
|
||||
|
||||
<div class="windows8">
|
||||
<div class="wBall" id="wBall_1">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_2">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_3">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_4">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
<div class="wBall" id="wBall_5">
|
||||
<div class="wInnerBall" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="error">{error}</div>
|
||||
<ToList {finish} />
|
||||
@ -1,49 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
const states = {
|
||||
approve: 1,
|
||||
enter: 2,
|
||||
};
|
||||
let state = states.approve;
|
||||
|
||||
let error = "";
|
||||
let code = "";
|
||||
export let number = "+4915...320";
|
||||
//export let finish;
|
||||
|
||||
function validateCode() {}
|
||||
|
||||
function sendCode() {
|
||||
// Send request to Server
|
||||
state = states.enter;
|
||||
//finish()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--error: red;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>SMS</h3>
|
||||
{#if state === states.approve}
|
||||
<p>Send SMS to {number}</p>
|
||||
<button class="btn btn-primary" on:click={sendCode}>Send</button>
|
||||
{:else}
|
||||
<p>A code was sent to you. Please enter</p>
|
||||
<input type="number" placeholder="Code" bind:value={code} />
|
||||
<button class="btn btn-primary" on:click={validateCode}>Send</button>
|
||||
<br />
|
||||
<a href="# " on:click|preventDefault={() => (state = states.approve)}>
|
||||
Not received?
|
||||
</a>
|
||||
{/if}
|
||||
<div class="error">{error}</div>
|
||||
|
||||
<ToList {finish} />
|
||||
@ -1,17 +0,0 @@
|
||||
<script>
|
||||
export let finish = () => {};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>
|
||||
<a href="# " on:click={evt => evt.preventDefault() || finish(false)}>
|
||||
Choose another Method
|
||||
</a>
|
||||
</p>
|
||||
@ -1,69 +0,0 @@
|
||||
<script>
|
||||
import ToList from "./toList.svelte";
|
||||
|
||||
export let finish;
|
||||
|
||||
const states = {
|
||||
getChallenge: 0,
|
||||
requestUser: 1,
|
||||
sendChallenge: 2,
|
||||
error: 3
|
||||
};
|
||||
|
||||
let state = states.getChallenge;
|
||||
|
||||
let error = "";
|
||||
|
||||
const onError = err => {
|
||||
state = states.error;
|
||||
error = err.message;
|
||||
};
|
||||
|
||||
let challenge;
|
||||
|
||||
async function requestUser() {
|
||||
state = states.requestUser;
|
||||
let res = await window.navigator.credentials.get({
|
||||
publicKey: challenge
|
||||
});
|
||||
state = states.sendChallenge();
|
||||
let r = res.response;
|
||||
let data = encode({
|
||||
authenticatorData: r.authenticatorData,
|
||||
clientDataJSON: r.clientDataJSON,
|
||||
signature: r.signature,
|
||||
userHandle: r.userHandle
|
||||
});
|
||||
let { success } = fetch("https://localhost:8444/auth", {
|
||||
body: data,
|
||||
method: "POST"
|
||||
}).then(res => res.json());
|
||||
if (success) {
|
||||
finish(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function getChallenge() {
|
||||
state = states.getChallenge;
|
||||
challenge = await fetch("https://localhost:8444/auth")
|
||||
.then(res => res.arrayBuffer())
|
||||
.then(data => decode(MessagePack.Buffer.from(data)));
|
||||
|
||||
requestUser().catch(onError);
|
||||
}
|
||||
getChallenge().catch(onError);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--error: red;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error);
|
||||
}
|
||||
</style>
|
||||
|
||||
<h3>U2F Security Key</h3>
|
||||
<h4>This Method is currently not supported. Please choose another one!</h4>
|
||||
<ToList {finish} />
|
||||
Reference in New Issue
Block a user