Add JRPC API, reworked Login and User pages
This commit is contained in:
@ -1,13 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import MainNavbar from "../../components/MainNavbar.svelte";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import { CurrentPage } from "./nav";
|
||||
import PersonalInfo from "./pages/PersonalInfo.svelte";
|
||||
import Security from "./pages/Security.svelte";
|
||||
|
||||
let sidebarOpen = false;
|
||||
let sidebarOpenVisible = false;
|
||||
|
||||
onMount(() => {
|
||||
const unsub = CurrentPage.subscribe(() => {
|
||||
sidebarOpen = false;
|
||||
});
|
||||
|
||||
return unsub;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grid main-grid min-h-screen overflow-hidden">
|
||||
<div class="col-span-2">
|
||||
<MainNavbar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||
</div>
|
||||
<div>
|
||||
<Sidebar />
|
||||
<Sidebar bind:sidebarOpen bind:sidebarOpenVisible />
|
||||
</div>
|
||||
<div class="overflow-auto p-4">
|
||||
{#if $CurrentPage == "personal-info"}
|
||||
@ -21,5 +37,6 @@
|
||||
<style>
|
||||
.main-grid {
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,197 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Account,
|
||||
Gender,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Heading,
|
||||
Spinner,
|
||||
Helper,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
let profileInfo: Account;
|
||||
let loadedProfileInfo: Account;
|
||||
let contactInfo: ContactInfo;
|
||||
|
||||
let loading = true;
|
||||
let error: string | undefined;
|
||||
|
||||
async function load() {
|
||||
error = undefined;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
profileInfo = await InternalAPI.Account.GetProfile();
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
contactInfo = await InternalAPI.Account.GetContactInfos();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let savingProfile = false;
|
||||
|
||||
async function saveProfileChanges() {
|
||||
savingProfile = true;
|
||||
|
||||
try {
|
||||
await new Promise((yes) => setTimeout(yes, 1000));
|
||||
await InternalAPI.Account.UpdateProfile(profileInfo);
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
savingProfile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: hasProfileChanged =
|
||||
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
let genders = [
|
||||
{
|
||||
value: Gender.None,
|
||||
name: "Not saying",
|
||||
},
|
||||
{
|
||||
value: Gender.Male,
|
||||
name: "Male",
|
||||
},
|
||||
{
|
||||
value: Gender.Female,
|
||||
name: "Female",
|
||||
},
|
||||
{
|
||||
value: Gender.Other,
|
||||
name: "Other",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
<Card>
|
||||
<Heading tag="h5">General Account Details</Heading>
|
||||
<hr class="mb-6" />
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!hasProfileChanged || savingProfile}
|
||||
on:click={saveProfileChanges}
|
||||
>
|
||||
{#if savingProfile}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card class="mt-4">
|
||||
<Heading tag="h5">Contact Details (WIP)</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
<Heading tag="h6" color="gray">Mails</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.mail as mail}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
|
||||
<Input
|
||||
id="mail-input"
|
||||
placeholder="Mail"
|
||||
bind:value={mail.mail}
|
||||
color={mail.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if mail.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Heading tag="h6" color="gray">Phones</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.phone as phone}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
|
||||
<Input
|
||||
id="phone-input"
|
||||
placeholder="Phone"
|
||||
bind:value={phone.phone}
|
||||
color={phone.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if phone.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> Phone is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- <div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div> -->
|
||||
|
||||
<!-- <Button>Save</Button> -->
|
||||
</Card>
|
||||
</Loading>
|
@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarGroup,
|
||||
@ -6,9 +6,29 @@
|
||||
SidebarWrapper,
|
||||
} from "flowbite-svelte";
|
||||
import { CurrentPage } from "./nav";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let sidebarOpen = false;
|
||||
export let sidebarOpenVisible = false;
|
||||
|
||||
$: open = !sidebarOpenVisible || sidebarOpen;
|
||||
|
||||
onMount(() => {
|
||||
const mq = window.matchMedia("(max-width: 768px)");
|
||||
const onChange = (e: MediaQueryListEvent) => {
|
||||
sidebarOpenVisible = e.matches;
|
||||
};
|
||||
mq.addEventListener("change", onChange);
|
||||
|
||||
onChange({ matches: mq.matches } as MediaQueryListEvent);
|
||||
|
||||
return () => {
|
||||
mq.removeEventListener("change", onChange);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<Sidebar class="h-screen">
|
||||
<Sidebar class="h-screen" style={open ? "display: block" : "display: none"}>
|
||||
<SidebarWrapper class="h-full">
|
||||
<SidebarGroup>
|
||||
<SidebarItem
|
||||
|
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
36
Frontend/src/pages/user/pages/AddTwoFactor.svelte
Normal file
@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Listgroup, ListgroupItem, Modal, Radio } from "flowbite-svelte";
|
||||
import Totp from "./TwoFactorRegistration/TOTP.svelte";
|
||||
import WebAuthn from "./TwoFactorRegistration/WebAuthn.svelte";
|
||||
|
||||
export let open: boolean;
|
||||
|
||||
let selectedType = undefined;
|
||||
$: {
|
||||
if (!open) {
|
||||
selectedType = undefined;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:open size="md" autoclose={false} class="w-full">
|
||||
{#if !selectedType}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select type
|
||||
</h3>
|
||||
<Listgroup active class="w-full">
|
||||
<ListgroupItem
|
||||
class="gap-2 px-4 py-4"
|
||||
on:click={() => (selectedType = "totp")}>TOTP</ListgroupItem
|
||||
>
|
||||
<ListgroupItem
|
||||
class="gap-2 px-4 py-4"
|
||||
on:click={() => (selectedType = "webauthn")}>WebAuthn</ListgroupItem
|
||||
>
|
||||
</Listgroup>
|
||||
{:else if selectedType == "totp"}
|
||||
<Totp on:reload />
|
||||
{:else if selectedType == "webauthn"}
|
||||
<WebAuthn on:reload />
|
||||
{/if}
|
||||
</Modal>
|
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
203
Frontend/src/pages/user/pages/PersonalInfo.svelte
Normal file
@ -0,0 +1,203 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Profile,
|
||||
Gender,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Label,
|
||||
Select,
|
||||
Heading,
|
||||
Spinner,
|
||||
Helper,
|
||||
} from "flowbite-svelte";
|
||||
|
||||
let profileInfo: Profile;
|
||||
let loadedProfileInfo: Profile;
|
||||
let contactInfo: ContactInfo;
|
||||
|
||||
let loading = true;
|
||||
let error: string | undefined;
|
||||
|
||||
async function load() {
|
||||
error = undefined;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
profileInfo = await InternalAPI.Account.GetProfile();
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
contactInfo = await InternalAPI.Account.GetContactInfos();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
let savingProfile = false;
|
||||
|
||||
async function saveProfileChanges() {
|
||||
savingProfile = true;
|
||||
|
||||
try {
|
||||
await new Promise((yes) => setTimeout(yes, 1000));
|
||||
await InternalAPI.Account.UpdateProfile(profileInfo);
|
||||
loadedProfileInfo = { ...profileInfo };
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
savingProfile = false;
|
||||
}
|
||||
}
|
||||
|
||||
$: hasProfileChanged =
|
||||
JSON.stringify(profileInfo) != JSON.stringify(loadedProfileInfo);
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
let genders = [
|
||||
{
|
||||
value: Gender.None,
|
||||
name: "Not saying",
|
||||
},
|
||||
{
|
||||
value: Gender.Male,
|
||||
name: "Male",
|
||||
},
|
||||
{
|
||||
value: Gender.Female,
|
||||
name: "Female",
|
||||
},
|
||||
{
|
||||
value: Gender.Other,
|
||||
name: "Other",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Card size="md" class="w-full">
|
||||
<Heading tag="h5">General Account Details</Heading>
|
||||
<hr class="mb-6" />
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input
|
||||
id="name-input"
|
||||
placeholder="Name"
|
||||
bind:value={profileInfo.name}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
disabled={!hasProfileChanged || savingProfile}
|
||||
on:click={saveProfileChanges}
|
||||
>
|
||||
{#if savingProfile}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Saving...
|
||||
{:else}
|
||||
Save
|
||||
{/if}
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card size="md" class="w-full">
|
||||
<Heading tag="h5">Contact Details (WIP)</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
<Heading tag="h6" color="gray">Mails</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.mail as mail}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="mail-input" class="block mb-2">Mail</Label> -->
|
||||
<Input
|
||||
id="mail-input"
|
||||
placeholder="Mail"
|
||||
bind:value={mail.mail}
|
||||
color={mail.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if mail.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> E-Mail is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> E-Mail needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<Heading tag="h6" color="gray">Phones</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#each contactInfo.phone as phone}
|
||||
<div class="mb-6">
|
||||
<!-- <Label for="phone-input" class="block mb-2">Phone</Label> -->
|
||||
<Input
|
||||
id="phone-input"
|
||||
placeholder="Phone"
|
||||
bind:value={phone.phone}
|
||||
color={phone.verified ? "green" : "base"}
|
||||
disabled
|
||||
/>
|
||||
{#if phone.verified}
|
||||
<Helper class="mt-2" color="green"
|
||||
><span class="font-medium">Well done!</span> Phone is verified.</Helper
|
||||
>
|
||||
{:else}
|
||||
<Helper class="mt-2" color="gray"
|
||||
><span class="font-medium">Oh no!</span> Phone needs verification.</Helper
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<!-- <div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={profileInfo.name} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="birthday-input" class="block mb-2">Birthday (WIP)</Label>
|
||||
<Input
|
||||
id="birthday-input"
|
||||
placeholder="Birthday"
|
||||
disabled
|
||||
bind:value={profileInfo.birthday}
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label class="block mb-2"
|
||||
>Gender
|
||||
<Select items={genders} bind:value={profileInfo.gender} />
|
||||
</Label>
|
||||
</div> -->
|
||||
|
||||
<!-- <Button>Save</Button> -->
|
||||
</Card>
|
||||
</div>
|
||||
</Loading>
|
@ -1,12 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
type ContactInfo,
|
||||
type Account,
|
||||
Gender,
|
||||
Token,
|
||||
TwoFactor,
|
||||
TFAType,
|
||||
} from "@hibas123/openauth-internalapi";
|
||||
import { Session, TFAOption, TFAType } from "@hibas123/openauth-internalapi";
|
||||
import InternalAPI from "../../../helper/api";
|
||||
import Loading from "../Loading.svelte";
|
||||
import { onMount } from "svelte";
|
||||
@ -27,18 +20,20 @@
|
||||
TableBodyCell,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
Alert,
|
||||
} from "flowbite-svelte";
|
||||
import AddTwoFactor from "./AddTwoFactor.svelte";
|
||||
|
||||
let tokens: Token[];
|
||||
let twofactors: TwoFactor[];
|
||||
let tokens: Session[];
|
||||
let twofactors: TFAOption[];
|
||||
let error: string | undefined;
|
||||
let loading = true;
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
try {
|
||||
tokens = await InternalAPI.Security.GetTokens();
|
||||
twofactors = await InternalAPI.Security.GetTwofactorOptions();
|
||||
tokens = await InternalAPI.Security.GetSessions();
|
||||
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
} finally {
|
||||
@ -46,13 +41,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
try {
|
||||
tokens = await InternalAPI.Security.GetSessions();
|
||||
twofactors = await InternalAPI.TwoFactor.GetOptions();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
load();
|
||||
});
|
||||
|
||||
async function revokeToken(id: string) {
|
||||
try {
|
||||
await InternalAPI.Security.RevokeToken(id);
|
||||
await InternalAPI.Security.RevokeSession(id);
|
||||
await load();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
@ -65,6 +69,49 @@
|
||||
[TFAType.BACKUP_CODE]: "Backup-Code",
|
||||
[TFAType.APP_ALLOW]: "App-Auth",
|
||||
};
|
||||
|
||||
let addTwoFactorOpen = false;
|
||||
|
||||
function openAddTwoFactor() {
|
||||
addTwoFactorOpen = true;
|
||||
}
|
||||
|
||||
async function deleteTwoFactor(id: string) {
|
||||
try {
|
||||
await InternalAPI.TwoFactor.Delete(id);
|
||||
await reload();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
let old_pw = "";
|
||||
let new_pw = "";
|
||||
let new_pw_repeat = "";
|
||||
|
||||
let change_password_success = false;
|
||||
let change_password_error: string | undefined;
|
||||
|
||||
function changePassword() {
|
||||
change_password_success = false;
|
||||
change_password_error = undefined;
|
||||
if (new_pw !== new_pw_repeat) {
|
||||
change_password_error = "Passwords do not match";
|
||||
return;
|
||||
}
|
||||
|
||||
InternalAPI.Security.ChangePassword(old_pw, new_pw)
|
||||
.then(() => {
|
||||
change_password_error = undefined;
|
||||
old_pw = "";
|
||||
new_pw = "";
|
||||
new_pw_repeat = "";
|
||||
change_password_success = true;
|
||||
})
|
||||
.catch((e) => {
|
||||
change_password_error = e.message;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<Loading {loading} {error}>
|
||||
@ -107,19 +154,31 @@
|
||||
<Heading tag="h5">Change Password</Heading>
|
||||
<hr class="mb-6" />
|
||||
|
||||
{#if change_password_success}
|
||||
<Alert color="green">Password changed successfully.</Alert>
|
||||
{/if}
|
||||
|
||||
{#if change_password_error}
|
||||
<Alert color="red">{change_password_error}</Alert>
|
||||
{/if}
|
||||
|
||||
<div class="mb-6">
|
||||
<Label for="oldPassword">Old Password</Label>
|
||||
<Input type="password" id="oldPassword" />
|
||||
<Input type="password" id="oldPassword" bind:value={old_pw} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="newPassword">New Password</Label>
|
||||
<Input type="password" id="newPassword" />
|
||||
<Input type="password" id="newPassword" bind:value={new_pw} />
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="newPasswordRepeat">Repeat New Password</Label>
|
||||
<Input type="password" id="newPasswordRepeat" />
|
||||
<Input
|
||||
type="password"
|
||||
id="newPasswordRepeat"
|
||||
bind:value={new_pw_repeat}
|
||||
/>
|
||||
</div>
|
||||
<Button class="mt-4">Change Password</Button>
|
||||
<Button class="mt-4" on:click={changePassword}>Change Password</Button>
|
||||
</Card>
|
||||
|
||||
<Card size="xl" class="mt-4">
|
||||
@ -130,12 +189,21 @@
|
||||
{#each twofactors as tfa}
|
||||
<AccordionItem>
|
||||
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span>
|
||||
<div>
|
||||
<Button
|
||||
color="red"
|
||||
class="mt-4"
|
||||
on:click={() => deleteTwoFactor(tfa.id)}>Delete</Button
|
||||
>
|
||||
</div>
|
||||
</AccordionItem>
|
||||
{/each}
|
||||
</Accordion>
|
||||
<Button class="mt-4">Add Option</Button>
|
||||
<Button class="mt-4" on:click={openAddTwoFactor}>Add Option</Button>
|
||||
</Card>
|
||||
|
||||
<AddTwoFactor on:reload={reload} bind:open={addTwoFactorOpen} />
|
||||
|
||||
<!-- <Card size="xl" class="mt-4">
|
||||
<Heading tag="h5">Delete Account</Heading>
|
||||
<hr class="mb-6" />
|
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
102
Frontend/src/pages/user/pages/TwoFactorRegistration/TOTP.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||
import InternalAPI from "../../../../helper/api";
|
||||
|
||||
import type { TFANewTOTP } from "@hibas123/openauth-internalapi";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let stage = "get-name";
|
||||
let name: string = "";
|
||||
let code: string = "";
|
||||
let totp: TFANewTOTP;
|
||||
|
||||
let creatingTOTP = false;
|
||||
let verifingTOTP = false;
|
||||
|
||||
async function createTOTP() {
|
||||
creatingTOTP = true;
|
||||
try {
|
||||
totp = await InternalAPI.TwoFactor.AddTOTP(name);
|
||||
stage = "verify";
|
||||
} catch (err) {
|
||||
} finally {
|
||||
creatingTOTP = false;
|
||||
}
|
||||
}
|
||||
|
||||
let verifyError = undefined;
|
||||
async function verifyTOTP() {
|
||||
verifingTOTP = true;
|
||||
verifyError = undefined;
|
||||
try {
|
||||
await InternalAPI.TwoFactor.VerifyTOTP(totp.id, code);
|
||||
stage = "done";
|
||||
dispatch("reload");
|
||||
} catch (err) {
|
||||
verifyError = err.message;
|
||||
code = "";
|
||||
} finally {
|
||||
verifingTOTP = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if stage == "get-name"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select a name
|
||||
</h3>
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<Button disabled={creatingTOTP} on:click={createTOTP}>
|
||||
{#if creatingTOTP}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||
{:else}
|
||||
Create
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "verify"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Save secret and verify
|
||||
</h3>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||
<div>Manually:</div>
|
||||
<div class="text-sm">
|
||||
{totp.secret}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="code-input" class="block mb-2">Code</Label>
|
||||
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||
</div>
|
||||
|
||||
{#if verifyError}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">{verifyError}</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button disabled={verifingTOTP} on:click={verifyTOTP}>
|
||||
{#if verifingTOTP}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||
{:else}
|
||||
Verify
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "done"}
|
||||
<Alert color="green">
|
||||
<h2 class="text-lg font-bold">Success</h2>
|
||||
<p class="mt-2">Your TOTP has been created.</p>
|
||||
</Alert>
|
||||
{:else}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">An unknown error occured.</p>
|
||||
</Alert>
|
||||
{/if}
|
@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import { Alert, Button, Input, Label, Spinner } from "flowbite-svelte";
|
||||
import { startRegistration } from "@simplewebauthn/browser";
|
||||
import InternalAPI from "../../../../helper/api";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let stage = "get-name";
|
||||
let name: string = "";
|
||||
|
||||
let creating = false;
|
||||
let error = undefined;
|
||||
|
||||
async function register() {
|
||||
creating = true;
|
||||
error = undefined;
|
||||
try {
|
||||
let challenge_data = await InternalAPI.TwoFactor.AddWebauthn(name);
|
||||
let challenge = JSON.parse(challenge_data.challenge);
|
||||
stage = "verify";
|
||||
|
||||
creating = false;
|
||||
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
console.log(challenge);
|
||||
let response = await startRegistration(challenge);
|
||||
|
||||
await InternalAPI.TwoFactor.VerifyWebAuthn(
|
||||
challenge_data.id,
|
||||
JSON.stringify(response)
|
||||
);
|
||||
|
||||
stage = "done";
|
||||
dispatch("reload");
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
console.error(err);
|
||||
} finally {
|
||||
creating = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">An unknown error occured.</p>
|
||||
</Alert>
|
||||
{:else if stage == "get-name"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select a name
|
||||
</h3>
|
||||
<div class="mb-6">
|
||||
<Label for="name-input" class="block mb-2">Name</Label>
|
||||
<Input id="name-input" placeholder="Name" bind:value={name} />
|
||||
</div>
|
||||
|
||||
<Button disabled={creating} on:click={register}>
|
||||
{#if creating}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Creating...
|
||||
{:else}
|
||||
Create
|
||||
{/if}
|
||||
</Button>
|
||||
{:else if stage == "verify"}
|
||||
<h3 class="text-xl font-medium text-gray-900 dark:text-white p-0">
|
||||
Select device to add
|
||||
</h3>
|
||||
<!-- <div class="flex flex-col justify-center items-center">
|
||||
<img class="w-64" src={totp.qr} alt="Secret {totp.secret}" />
|
||||
<div>Manually:</div>
|
||||
<div class="text-sm">
|
||||
{totp.secret}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<Label for="code-input" class="block mb-2">Code</Label>
|
||||
<Input id="code-input" placeholder="Code" bind:value={code} />
|
||||
</div>
|
||||
|
||||
{#if verifyError}
|
||||
<Alert color="red">
|
||||
<h2 class="text-lg font-bold">Error</h2>
|
||||
<p class="mt-2">{verifyError}</p>
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
<Button disabled={verifing} on:click={verifyTOTP}>
|
||||
{#if verifing}
|
||||
<Spinner class="mr-3" size="4" color="white" /> Verify...
|
||||
{:else}
|
||||
Verify
|
||||
{/if}
|
||||
</Button> -->
|
||||
{:else if stage == "done"}
|
||||
<Alert color="green">
|
||||
<h2 class="text-lg font-bold">Success</h2>
|
||||
<p class="mt-2">Your WebAuthn device has been registered.</p>
|
||||
</Alert>
|
||||
{/if}
|
Reference in New Issue
Block a user