Start implementing a new user page for account and security settings

This commit is contained in:
Fabian Stamm
2023-04-09 18:20:43 +02:00
parent 1e2bb83447
commit 922ed1e813
46 changed files with 2307 additions and 443 deletions

View File

@ -1,18 +1,3 @@
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li > a {
text-decoration: none;
}
</style>
<div class="main">
<h1>Home Page</h1>
@ -42,3 +27,18 @@
</li>
</ul>
</div>
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li > a {
text-decoration: none;
}
</style>

View File

@ -1,207 +1,25 @@
<script>
import AccountPage from "./Pages/Account.svelte";
import SecurityPage from "./Pages/Security.svelte";
import { slide, fade } from "svelte/transition";
const pages = [
{
id: "account",
title: "Account",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgdmlld0JveD0iMCAwIDUwMCA1MDAiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT4wMSBAZnVsbHdpZHRoPC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj48ZyBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDU3LjUsMjUwIEM0NTcuNSwxMzUuOTUzMTk5IDM2NS4wNDY4MDEsNDMuNSAyNTEsNDMuNSBDMTM2Ljk1MzE5OSw0My41IDQ0LjUsMTM1Ljk1MzE5OSA0NC41LDI1MCBDNDQuNSwzNjQuMDQ2ODAxIDEzNi45NTMxOTksNDU2LjUgMjUxLDQ1Ni41IEMzNjUuMDQ2ODAxLDQ1Ni41IDQ1Ny41LDM2NC4wNDY4MDEgNDU3LjUsMjUwIFogTTU3LjUsMjUwIEM1Ny41LDE0My4xMzI5MDEgMTQ0LjEzMjkwMSw1Ni41IDI1MSw1Ni41IEMzNTcuODY3MDk5LDU2LjUgNDQ0LjUsMTQzLjEzMjkwMSA0NDQuNSwyNTAgQzQ0NC41LDM1Ni44NjcwOTkgMzU3Ljg2NzA5OSw0NDMuNSAyNTEsNDQzLjUgQzE0NC4xMzI5MDEsNDQzLjUgNTcuNSwzNTYuODY3MDk5IDU3LjUsMjUwIFoiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD48cGF0aCBkPSJNMjUxLjUsMjUyLjkzMzk2MiBDMTk2Ljg1NDE5LDI1Mi45MzM5NjIgMTUyLjUsMjk2LjM2MDgwOSAxNTIuNSwzNTAgQzE1Mi41LDM1My41ODk4NTEgMTU1LjQxMDE0OSwzNTYuNSAxNTksMzU2LjUgTDM0NCwzNTYuNSBDMzQ3LjU4OTg1MSwzNTYuNSAzNTAuNSwzNTMuNTg5ODUxIDM1MC41LDM1MCBDMzUwLjUsMjk2LjM2MDgwOSAzMDYuMTQ1ODEsMjUyLjkzMzk2MiAyNTEuNSwyNTIuOTMzOTYyIFogTTE2NS41LDM0My41MDAwMDEgQzE2NS41LDMwMy42MDI3MDggMjAzLjk3MzEzMSwyNjUuOTMzOTYyIDI1MS41LDI2NS45MzM5NjIgQzI5OS4wMjY4NjksMjY1LjkzMzk2MiAzMzcuNSwzMDMuNjAyNzA4IDMzNy40OTk5OTcsMzQzLjUwMDAwMSBMMTY1LjUsMzQzLjUwMDAwMSBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PHBhdGggZD0iTTMwNC4yNSwxOTMuMzk2MjI2IEMzMDQuMjUsMTY1LjgwODIwMiAyODEuNDY1NDI0LDE0My41IDI1My40MjcwODMsMTQzLjUgQzIyNS4zODg3NDIsMTQzLjUgMjAyLjYwNDE2NywxNjUuODA4MjAyIDIwMi42MDQxNjcsMTkzLjM5NjIyNiBDMjAyLjYwNDE2NywyMjAuOTg0MjUgMjI1LjM4ODc0MiwyNDMuMjkyNDUzIDI1My40MjcwODMsMjQzLjI5MjQ1MyBDMjgxLjQ2NTQyNCwyNDMuMjkyNDUzIDMwNC4yNSwyMjAuOTg0MjUgMzA0LjI1LDE5My4zOTYyMjYgWiBNMjE1LjYwNDE2NywxOTMuMzk2MjI2IEMyMTUuNjA0MTY3LDE3My4wNTAxMDIgMjMyLjUwNzY4MywxNTYuNSAyNTMuNDI3MDgzLDE1Ni41IEMyNzQuMzQ2NDg0LDE1Ni41IDI5MS4yNSwxNzMuMDUwMTAyIDI5MS4yNSwxOTMuMzk2MjI2IEMyOTEuMjUsMjEzLjc0MjM1MSAyNzQuMzQ2NDg0LDIzMC4yOTI0NTMgMjUzLjQyNzA4MywyMzAuMjkyNDUzIEMyMzIuNTA3NjgzLDIzMC4yOTI0NTMgMjE1LjYwNDE2NywyMTMuNzQyMzUxIDIxNS42MDQxNjcsMTkzLjM5NjIyNiBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PC9nPjwvZz48L3N2Zz4=",
component: AccountPage,
},
{
id: "security",
title: "Security",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik00NDUuOCwzNy4zYy0xLjItMC43LTIuNy0wLjgtMy45LTAuMWMtNC4zLDIuMi0xMS42LDMuNC0yMS45LDMuNGMtMzMuNiwwLTkwLjgtMTIuNC0xMjguNy0yMC41Yy0xNC0zLTI2LjEtNS42LTM0LjEtNyAgIGMtMC40LTAuMS0wLjktMC4xLTEuNCwwYy03LjIsMS4yLTE4LjUsMy42LTMxLjcsNi4zQzE4NC4zLDI3LjYsMTI0LjIsNDAsOTAuNiw0MGMtMTEuNiwwLTE3LTEuNS0xOS41LTIuOCAgIGMtMS4yLTAuNi0yLjctMC42LTMuOSwwLjFzLTEuOSwyLTEuOSwzLjRjMCw3My4xLDMuOCwxNjguNCwzMy45LDI1Ny43YzE0LjMsNDIuOCwzMy41LDgwLjcsNTcuMSwxMTIuNyAgIGMyNi42LDM2LDU5LjcsNjUuNyw5OC4zLDg4LjNjMC42LDAuNCwxLjMsMC41LDIsMC41czEuNC0wLjIsMi0wLjVjMzguNi0yMi42LDcxLjYtNTIuMyw5OC4yLTg4LjNjMjMuNi0zMiw0Mi45LTY5LjksNTcuMS0xMTIuNyAgIGMyOS41LTg3LjYsMzMuNy0xNzkuNCwzMy45LTI1Ny43QzQ0Ny43LDM5LjQsNDQ3LDM4LjEsNDQ1LjgsMzcuM3ogTTQwNi4zLDI5NS45Yy0yOS4zLDg4LjEtNzkuNywxNTMuOC0xNDkuOCwxOTUuNCAgIEMxODYuNCw0NDkuNywxMzYsMzg0LDEwNi43LDI5NS45Qzc3LjgsMjEwLDczLjQsMTE4LjEsNzMuMiw0Ni40YzQuNSwxLjEsMTAuMiwxLjYsMTcuMywxLjZjMCwwLDAsMCwwLDAgICBjMzQuNSwwLDk1LjEtMTIuNiwxMzUuMi0yMC45YzEyLjctMi42LDIzLjYtNC45LDMwLjctNi4xYzcuOCwxLjQsMTkuNSwzLjksMzMuMSw2LjhDMzMwLDM2LjYsMzg1LjUsNDguNiw0MjAsNDguNiAgIGM4LjEsMCwxNC43LTAuNywxOS43LTJDNDM5LjMsMTIyLjksNDM0LjcsMjExLjUsNDA2LjMsMjk1Ljl6Ij48L3BhdGg+PHBhdGggZD0iTTI1Ni41LDIxNy44YzQ1LDAsODEuNi0zNi42LDgxLjYtODEuNmMwLTQ1LTM2LjYtODEuNi04MS42LTgxLjZjLTQ1LDAtODEuNiwzNi42LTgxLjYsODEuNiAgIEMxNzQuOSwxODEuMiwyMTEuNSwyMTcuOCwyNTYuNSwyMTcuOHogTTI1Ni41LDYyLjZjNDAuNiwwLDczLjYsMzMsNzMuNiw3My42YzAsNDAuNi0zMyw3My42LTczLjYsNzMuNmMtNDAuNiwwLTczLjYtMzMtNzMuNi03My42ICAgQzE4Mi45LDk1LjYsMjE1LjksNjIuNiwyNTYuNSw2Mi42eiI+PC9wYXRoPjxwYXRoIGQ9Ik0zMDkuMiwyMjguOUgyMDMuOGMtMjYuNSwwLTQ4LDIxLjUtNDgsNDh2NzljMCwyLjIsMS44LDQsNCw0aDE5My40YzIuMiwwLDQtMS44LDQtNHYtNzkgICBDMzU3LjIsMjUwLjQsMzM1LjYsMjI4LjksMzA5LjIsMjI4Ljl6IE0zNDkuMiwzNTEuOUgxNjMuOHYtNzVjMC0yMiwxNy45LTQwLDQwLTQwaDEwNS40YzIyLjEsMCw0MCwxNy45LDQwLDQwTDM0OS4yLDM1MS45ICAgTDM0OS4yLDM1MS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=",
component: SecurityPage,
},
];
function getPage() {
let pageid = window.location.hash.slice(1);
return pages.find((e) => e.id === pageid) || pages[0];
}
let page = getPage();
window.addEventListener("hashchange", () => {
page = getPage();
});
// $: title = pages.find(e => e.id === page).title;
const mq = window.matchMedia("(min-width: 45rem)");
let sidebar_button = !mq.matches;
mq.addEventListener("change", (ev) => {
sidebar_button = !ev.matches;
});
let sidebar_active = false;
function setPage(pageid) {
let pg = pages.find((e) => e.id === pageid);
if (!pg) {
throw new Error("Invalid Page " + pageid);
} else {
let url = new URL(window.location.href);
url.hash = pg.id;
window.history.pushState({}, pg.title, url);
page = getPage();
}
sidebar_active = false;
}
let loading = true;
import NavigationBar from "./NavigationBar.svelte";
<script lang="ts">
import Sidebar from "./Sidebar.svelte";
import { CurrentPage } from "./nav";
import PersonalInfo from "./pages/PersonalInfo.svelte";
import Security from "./pages/Security.svelte";
</script>
<div class:loading class="root">
<div class="app_container">
<div class="header">
{#if sidebar_button}
<button on:click={() => (sidebar_active = !sidebar_active)}>
<svg
id="Layer_1"
style="enable-background:new 0 0 32 32;"
version="1.1"
viewBox="0 0 32 32"
width="32px"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z
M28,14H4c-1.104,0-2,0.896-2,2
s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z
M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2
S29.104,22,28,22z"
/>
</svg>
</button>
{/if}
<h1>{page.title}</h1>
</div>
<div class="sidebar" class:sidebar-visible={sidebar_active}>
<NavigationBar open={setPage} {pages} active={page} />
</div>
<div class="content">
<svelte:component this={page.component} bind:loading />
</div>
<div class="footer" />
<div class="grid main-grid min-h-screen overflow-hidden">
<div>
<Sidebar />
</div>
<div class="overflow-auto p-4">
{#if $CurrentPage == "personal-info"}
<PersonalInfo />
{:else if $CurrentPage == "security"}
<Security />
{/if}
</div>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader" />
</div>
</div>
{/if}
<style>
.loading {
background-color: rgba(0, 0, 0, 0.04);
filter: blur(10px);
}
:root {
--sidebar-width: 250px;
}
.root {
height: 100%;
}
.app_container {
display: grid;
height: 100%;
grid-template-columns: auto 100%;
grid-template-rows: 60px auto 60px;
grid-template-areas:
"sidebar header"
"sidebar mc"
"sidebar footer";
}
.header {
grid-area: header;
background-color: var(--primary);
padding: 12px;
display: flex;
}
.header > h1 {
margin: 0;
padding: 0;
font-size: 24px;
line-height: 36px;
color: white;
margin-left: 2rem;
}
.header > button {
height: 36px;
background-color: transparent;
border: none;
font-size: 20px;
}
.header > button:hover {
background-color: rgba(255, 255, 255, 0.151);
}
.sidebar {
width: 0;
overflow: hidden;
grid-area: sidebar;
transition: width 0.2s;
background-color: lightgrey;
height: 100%;
}
.sidebar-visible {
width: var(--sidebar-width);
transition: width 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
grid-area: mc;
padding: 1rem;
}
.footer {
grid-area: footer;
}
@media (min-width: 45rem) {
.app_container {
grid-template-columns: auto 1fr;
}
.sidebar {
width: var(--sidebar-width);
transition: all 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
padding: 2rem;
}
}
.loader_container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
.main-grid {
grid-template-columns: auto 1fr;
}
</style>

View File

@ -0,0 +1,19 @@
<script lang="ts">
import { Alert, Spinner } from "flowbite-svelte";
export let loading: boolean;
export let error: string | undefined;
</script>
{#if loading}
<div class="h-full flex justify-center items-center">
<Spinner size={"16"} />
</div>
{:else if error}
<Alert color="red">
<span class="font-medium">Error occured!</span>
{error}
</Alert>
{:else}
<slot />
{/if}

View File

@ -0,0 +1,197 @@
<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>

View File

@ -1,188 +1,149 @@
<script context="module">
const TFATypes = new Map();
TFATypes.set(0, "Authenticator");
TFATypes.set(1, "Backup Codes");
TFATypes.set(2, "YubiKey");
TFATypes.set(3, "Push Notification");
</script>
<script lang="ts">
import {
type ContactInfo,
type Account,
Gender,
Token,
TwoFactor,
TFAType,
} 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,
Table,
TableHead,
TableHeadCell,
TableBody,
TableBodyRow,
TableBodyCell,
Accordion,
AccordionItem,
} from "flowbite-svelte";
<script>
import Box from "./Box.svelte";
import BoxItem from "./BoxItem.svelte";
import NextIcon from "./NextIcon.svelte";
import request from "../../../helper/request.ts";
let tokens: Token[];
let twofactors: TwoFactor[];
let error: string | undefined;
let loading = true;
export let loading = false;
let twofactor = [];
async function deleteTFA(id) {
let res = await request(
"/api/user/twofactor/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadTwoFactor();
}
async function loadTwoFactor() {
let res = await request(
"/api/user/twofactor",
undefined,
undefined,
undefined,
true,
true
);
twofactor = res.methods;
}
let token = [];
async function revoke(id) {
let res = await request(
"/api/user/token/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadToken();
}
async function loadToken() {
async function load() {
loading = true;
let res = await request(
"/api/user/token",
undefined,
undefined,
undefined,
true,
true
);
token = res.token;
loading = false;
try {
tokens = await InternalAPI.Security.GetTokens();
twofactors = await InternalAPI.Security.GetTwofactorOptions();
} catch (e) {
error = e.message;
} finally {
loading = false;
}
}
loadToken();
loadTwoFactor();
onMount(() => {
load();
});
async function revokeToken(id: string) {
try {
await InternalAPI.Security.RevokeToken(id);
await load();
} catch (e) {
error = e.message;
}
}
const typeToName = {
[TFAType.TOTP]: "TOTP",
[TFAType.WEBAUTHN]: "Security Key (WebAuthn)",
[TFAType.BACKUP_CODE]: "Backup-Code",
[TFAType.APP_ALLOW]: "App-Auth",
};
</script>
<style>
.btn {
background-color: var(--primary);
margin: auto 0;
margin-left: 1rem;
font-size: 1rem;
padding: 0 0.5rem;
}
<Loading {loading} {error}>
<Card size="xl">
<Heading tag="h5">Active Sessions</Heading>
<hr class="mb-6" />
<Table>
<TableHead>
<TableHeadCell>Browser</TableHeadCell>
<TableHeadCell class="w-full">IP</TableHeadCell>
<TableHeadCell class="material-icons-outlined w-20"
>delete</TableHeadCell
>
</TableHead>
<TableBody>
{#each tokens as token}
<TableBodyRow
class="bg-yellow-50"
color={token.isthis ? "custom" : "default"}
>
<TableBodyCell>{token.browser}</TableBodyCell>
<TableBodyCell>{token.ip}</TableBodyCell>
<TableBodyCell>
<!-- svelte-ignore a11y-missing-attribute -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<a
class="font-medium text-red-600 hover:underline dark:text-blue-500"
on:click={() => revokeToken(token.id)}
>
Revoke
</a>
</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
</Table>
</Card>
.floating {
margin-bottom: 0;
}
<Card size="xl" class="mt-4">
<Heading tag="h5">Change Password</Heading>
<hr class="mb-6" />
.input-container {
display: flex;
}
<div class="mb-6">
<Label for="oldPassword">Old Password</Label>
<Input type="password" id="oldPassword" />
</div>
<div class="mb-6">
<Label for="newPassword">New Password</Label>
<Input type="password" id="newPassword" />
</div>
<div class="mb-6">
<Label for="newPasswordRepeat">Repeat New Password</Label>
<Input type="password" id="newPasswordRepeat" />
</div>
<Button class="mt-4">Change Password</Button>
</Card>
.input-container > *:first-child {
flex-grow: 1;
}
<Card size="xl" class="mt-4">
<Heading tag="h5">Two Factor Auth</Heading>
<hr class="mb-6" />
select {
background-color: unset;
border: 0;
border-radius: 0;
color: unset;
font-size: unset;
border-bottom: 1px solid #757575;
/* Firefox */
-moz-appearance: none;
/* Safari and Chrome */
-webkit-appearance: none;
appearance: none;
height: 100%;
width: 100%;
}
<Accordion>
{#each twofactors as tfa}
<AccordionItem>
<span slot="header">{tfa.name ?? typeToName[tfa.tfatype]}</span>
</AccordionItem>
{/each}
</Accordion>
<Button class="mt-4">Add Option</Button>
</Card>
select > option {
background-color: unset;
}
<!-- <Card size="xl" class="mt-4">
<Heading tag="h5">Delete Account</Heading>
<hr class="mb-6" />
.select-wrapper {
position: relative;
}
.select-wrapper::after {
content: ">";
display: block;
position: absolute;
right: 2rem;
top: 0;
bottom: 0;
width: 1rem;
transform: rotate(90deg) scaleY(2);
}
</style>
<Box>
<h1>Two Factor</h1>
<BoxItem name="Add new" open={false} />
{#each twofactor as t}
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => deleteTFA(t.id)}>
Delete
</button>
</BoxItem>
{/each}
<!-- <BoxItem name="Name" value={name} open={false}>
<div class="input-container">
<div class="floating group">
<input type="text" autocomplete="username" bind:value={name}>
<span class="highlight"></span>
<span class="bar"></span>
<label>Name</label>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Gender" value={gender} open={true}>
<div class="input-container">
<div class="select-wrapper">
<select>
<option value="1">Male</option>
<option value="2">Female</option>
<option value="3">Other</option>
</select>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Birthday" value={birthday} />
<BoxItem name="Password" value="******" /> -->
</Box>
<Box>
<h1>Anmeldungen</h1>
{#each token as t}
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => revoke(t.id)}>
Revoke
</button>
</BoxItem>
{:else}<span>No Tokens</span>{/each}
<!-- <BoxItem name="E-Mail" value={email} />
<BoxItem name="Phone" value={phone} /> -->
</Box>
<div class="mb-6">
<Label for="password">Password</Label>
<Input type="password" id="password" />
</div>
<Button class="mt-4">Delete Account</Button>
</Card> -->
</Loading>

View File

@ -0,0 +1,34 @@
<script>
import {
Sidebar,
SidebarGroup,
SidebarItem,
SidebarWrapper,
} from "flowbite-svelte";
import { CurrentPage } from "./nav";
</script>
<Sidebar class="h-screen">
<SidebarWrapper class="h-full">
<SidebarGroup>
<SidebarItem
label="Personal Data"
active={$CurrentPage == "personal-info"}
href="#personal-info"
>
<svelte:fragment slot="icon">
<span class="material-icons-outlined"> account_circle </span>
</svelte:fragment>
</SidebarItem>
<SidebarItem
label="Security"
active={$CurrentPage == "security"}
href="#security"
>
<svelte:fragment slot="icon">
<span class="material-icons-outlined"> lock </span>
</svelte:fragment>
</SidebarItem>
</SidebarGroup>
</SidebarWrapper>
</Sidebar>

View File

@ -1,4 +1,4 @@
import "../../components/theme";
import "../../main.css";
import App from "./App.svelte";
new App({

View File

@ -0,0 +1,25 @@
import { writable } from "svelte/store";
type Pages = "personal-info" | "security";
function getCurrentPage(): Pages | undefined {
let hash = window.location.hash;
if (hash.length > 0) {
hash = hash.substring(1);
if (hash === "personal-info" || hash === "security") {
return hash;
}
}
}
export const CurrentPage = writable<Pages>(getCurrentPage() ?? "personal-info");
window.addEventListener("hashchange", () => {
CurrentPage.set(getCurrentPage() ?? "personal-info");
});
export function navigateTo(page: Pages) {
window.location.hash = "#" + page;
}

View File

@ -0,0 +1,207 @@
<script>
import AccountPage from "./Pages/Account.svelte";
import SecurityPage from "./Pages/Security.svelte";
import { slide, fade } from "svelte/transition";
const pages = [
{
id: "account",
title: "Account",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgdmlld0JveD0iMCAwIDUwMCA1MDAiIHZlcnNpb249IjEuMSIgeD0iMHB4IiB5PSIwcHgiPjx0aXRsZT4wMSBAZnVsbHdpZHRoPC90aXRsZT48ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz48ZyBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBza2V0Y2g6dHlwZT0iTVNQYWdlIj48ZyBza2V0Y2g6dHlwZT0iTVNBcnRib2FyZEdyb3VwIiBmaWxsPSIjMDAwMDAwIj48cGF0aCBkPSJNNDU3LjUsMjUwIEM0NTcuNSwxMzUuOTUzMTk5IDM2NS4wNDY4MDEsNDMuNSAyNTEsNDMuNSBDMTM2Ljk1MzE5OSw0My41IDQ0LjUsMTM1Ljk1MzE5OSA0NC41LDI1MCBDNDQuNSwzNjQuMDQ2ODAxIDEzNi45NTMxOTksNDU2LjUgMjUxLDQ1Ni41IEMzNjUuMDQ2ODAxLDQ1Ni41IDQ1Ny41LDM2NC4wNDY4MDEgNDU3LjUsMjUwIFogTTU3LjUsMjUwIEM1Ny41LDE0My4xMzI5MDEgMTQ0LjEzMjkwMSw1Ni41IDI1MSw1Ni41IEMzNTcuODY3MDk5LDU2LjUgNDQ0LjUsMTQzLjEzMjkwMSA0NDQuNSwyNTAgQzQ0NC41LDM1Ni44NjcwOTkgMzU3Ljg2NzA5OSw0NDMuNSAyNTEsNDQzLjUgQzE0NC4xMzI5MDEsNDQzLjUgNTcuNSwzNTYuODY3MDk5IDU3LjUsMjUwIFoiIHNrZXRjaDp0eXBlPSJNU1NoYXBlR3JvdXAiPjwvcGF0aD48cGF0aCBkPSJNMjUxLjUsMjUyLjkzMzk2MiBDMTk2Ljg1NDE5LDI1Mi45MzM5NjIgMTUyLjUsMjk2LjM2MDgwOSAxNTIuNSwzNTAgQzE1Mi41LDM1My41ODk4NTEgMTU1LjQxMDE0OSwzNTYuNSAxNTksMzU2LjUgTDM0NCwzNTYuNSBDMzQ3LjU4OTg1MSwzNTYuNSAzNTAuNSwzNTMuNTg5ODUxIDM1MC41LDM1MCBDMzUwLjUsMjk2LjM2MDgwOSAzMDYuMTQ1ODEsMjUyLjkzMzk2MiAyNTEuNSwyNTIuOTMzOTYyIFogTTE2NS41LDM0My41MDAwMDEgQzE2NS41LDMwMy42MDI3MDggMjAzLjk3MzEzMSwyNjUuOTMzOTYyIDI1MS41LDI2NS45MzM5NjIgQzI5OS4wMjY4NjksMjY1LjkzMzk2MiAzMzcuNSwzMDMuNjAyNzA4IDMzNy40OTk5OTcsMzQzLjUwMDAwMSBMMTY1LjUsMzQzLjUwMDAwMSBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PHBhdGggZD0iTTMwNC4yNSwxOTMuMzk2MjI2IEMzMDQuMjUsMTY1LjgwODIwMiAyODEuNDY1NDI0LDE0My41IDI1My40MjcwODMsMTQzLjUgQzIyNS4zODg3NDIsMTQzLjUgMjAyLjYwNDE2NywxNjUuODA4MjAyIDIwMi42MDQxNjcsMTkzLjM5NjIyNiBDMjAyLjYwNDE2NywyMjAuOTg0MjUgMjI1LjM4ODc0MiwyNDMuMjkyNDUzIDI1My40MjcwODMsMjQzLjI5MjQ1MyBDMjgxLjQ2NTQyNCwyNDMuMjkyNDUzIDMwNC4yNSwyMjAuOTg0MjUgMzA0LjI1LDE5My4zOTYyMjYgWiBNMjE1LjYwNDE2NywxOTMuMzk2MjI2IEMyMTUuNjA0MTY3LDE3My4wNTAxMDIgMjMyLjUwNzY4MywxNTYuNSAyNTMuNDI3MDgzLDE1Ni41IEMyNzQuMzQ2NDg0LDE1Ni41IDI5MS4yNSwxNzMuMDUwMTAyIDI5MS4yNSwxOTMuMzk2MjI2IEMyOTEuMjUsMjEzLjc0MjM1MSAyNzQuMzQ2NDg0LDIzMC4yOTI0NTMgMjUzLjQyNzA4MywyMzAuMjkyNDUzIEMyMzIuNTA3NjgzLDIzMC4yOTI0NTMgMjE1LjYwNDE2NywyMTMuNzQyMzUxIDIxNS42MDQxNjcsMTkzLjM5NjIyNiBaIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIj48L3BhdGg+PC9nPjwvZz48L3N2Zz4=",
component: AccountPage,
},
{
id: "security",
title: "Security",
icon: "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9JzMwMHB4JyB3aWR0aD0nMzAwcHgnICBmaWxsPSIjMDAwMDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjEiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgNTEyIDUxMiIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNTEyIDUxMjsiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGQ9Ik00NDUuOCwzNy4zYy0xLjItMC43LTIuNy0wLjgtMy45LTAuMWMtNC4zLDIuMi0xMS42LDMuNC0yMS45LDMuNGMtMzMuNiwwLTkwLjgtMTIuNC0xMjguNy0yMC41Yy0xNC0zLTI2LjEtNS42LTM0LjEtNyAgIGMtMC40LTAuMS0wLjktMC4xLTEuNCwwYy03LjIsMS4yLTE4LjUsMy42LTMxLjcsNi4zQzE4NC4zLDI3LjYsMTI0LjIsNDAsOTAuNiw0MGMtMTEuNiwwLTE3LTEuNS0xOS41LTIuOCAgIGMtMS4yLTAuNi0yLjctMC42LTMuOSwwLjFzLTEuOSwyLTEuOSwzLjRjMCw3My4xLDMuOCwxNjguNCwzMy45LDI1Ny43YzE0LjMsNDIuOCwzMy41LDgwLjcsNTcuMSwxMTIuNyAgIGMyNi42LDM2LDU5LjcsNjUuNyw5OC4zLDg4LjNjMC42LDAuNCwxLjMsMC41LDIsMC41czEuNC0wLjIsMi0wLjVjMzguNi0yMi42LDcxLjYtNTIuMyw5OC4yLTg4LjNjMjMuNi0zMiw0Mi45LTY5LjksNTcuMS0xMTIuNyAgIGMyOS41LTg3LjYsMzMuNy0xNzkuNCwzMy45LTI1Ny43QzQ0Ny43LDM5LjQsNDQ3LDM4LjEsNDQ1LjgsMzcuM3ogTTQwNi4zLDI5NS45Yy0yOS4zLDg4LjEtNzkuNywxNTMuOC0xNDkuOCwxOTUuNCAgIEMxODYuNCw0NDkuNywxMzYsMzg0LDEwNi43LDI5NS45Qzc3LjgsMjEwLDczLjQsMTE4LjEsNzMuMiw0Ni40YzQuNSwxLjEsMTAuMiwxLjYsMTcuMywxLjZjMCwwLDAsMCwwLDAgICBjMzQuNSwwLDk1LjEtMTIuNiwxMzUuMi0yMC45YzEyLjctMi42LDIzLjYtNC45LDMwLjctNi4xYzcuOCwxLjQsMTkuNSwzLjksMzMuMSw2LjhDMzMwLDM2LjYsMzg1LjUsNDguNiw0MjAsNDguNiAgIGM4LjEsMCwxNC43LTAuNywxOS43LTJDNDM5LjMsMTIyLjksNDM0LjcsMjExLjUsNDA2LjMsMjk1Ljl6Ij48L3BhdGg+PHBhdGggZD0iTTI1Ni41LDIxNy44YzQ1LDAsODEuNi0zNi42LDgxLjYtODEuNmMwLTQ1LTM2LjYtODEuNi04MS42LTgxLjZjLTQ1LDAtODEuNiwzNi42LTgxLjYsODEuNiAgIEMxNzQuOSwxODEuMiwyMTEuNSwyMTcuOCwyNTYuNSwyMTcuOHogTTI1Ni41LDYyLjZjNDAuNiwwLDczLjYsMzMsNzMuNiw3My42YzAsNDAuNi0zMyw3My42LTczLjYsNzMuNmMtNDAuNiwwLTczLjYtMzMtNzMuNi03My42ICAgQzE4Mi45LDk1LjYsMjE1LjksNjIuNiwyNTYuNSw2Mi42eiI+PC9wYXRoPjxwYXRoIGQ9Ik0zMDkuMiwyMjguOUgyMDMuOGMtMjYuNSwwLTQ4LDIxLjUtNDgsNDh2NzljMCwyLjIsMS44LDQsNCw0aDE5My40YzIuMiwwLDQtMS44LDQtNHYtNzkgICBDMzU3LjIsMjUwLjQsMzM1LjYsMjI4LjksMzA5LjIsMjI4Ljl6IE0zNDkuMiwzNTEuOUgxNjMuOHYtNzVjMC0yMiwxNy45LTQwLDQwLTQwaDEwNS40YzIyLjEsMCw0MCwxNy45LDQwLDQwTDM0OS4yLDM1MS45ICAgTDM0OS4yLDM1MS45eiI+PC9wYXRoPjwvZz48L3N2Zz4=",
component: SecurityPage,
},
];
function getPage() {
let pageid = window.location.hash.slice(1);
return pages.find((e) => e.id === pageid) || pages[0];
}
let page = getPage();
window.addEventListener("hashchange", () => {
page = getPage();
});
// $: title = pages.find(e => e.id === page).title;
const mq = window.matchMedia("(min-width: 45rem)");
let sidebar_button = !mq.matches;
mq.addEventListener("change", (ev) => {
sidebar_button = !ev.matches;
});
let sidebar_active = false;
function setPage(pageid) {
let pg = pages.find((e) => e.id === pageid);
if (!pg) {
throw new Error("Invalid Page " + pageid);
} else {
let url = new URL(window.location.href);
url.hash = pg.id;
window.history.pushState({}, pg.title, url);
page = getPage();
}
sidebar_active = false;
}
let loading = true;
import NavigationBar from "./NavigationBar.svelte";
</script>
<div class:loading class="root">
<div class="app_container">
<div class="header">
{#if sidebar_button}
<button on:click={() => (sidebar_active = !sidebar_active)}>
<svg
id="Layer_1"
style="enable-background:new 0 0 32 32;"
version="1.1"
viewBox="0 0 32 32"
width="32px"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<path
d="M4,10h24c1.104,0,2-0.896,2-2s-0.896-2-2-2H4C2.896,6,2,6.896,2,8S2.896,10,4,10z
M28,14H4c-1.104,0-2,0.896-2,2
s0.896,2,2,2h24c1.104,0,2-0.896,2-2S29.104,14,28,14z
M28,22H4c-1.104,0-2,0.896-2,2s0.896,2,2,2h24c1.104,0,2-0.896,2-2
S29.104,22,28,22z"
/>
</svg>
</button>
{/if}
<h1>{page.title}</h1>
</div>
<div class="sidebar" class:sidebar-visible={sidebar_active}>
<NavigationBar open={setPage} {pages} active={page} />
</div>
<div class="content">
<svelte:component this={page.component} bind:loading />
</div>
<div class="footer" />
</div>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader" />
</div>
</div>
{/if}
<style>
.loading {
background-color: rgba(0, 0, 0, 0.04);
filter: blur(10px);
}
:root {
--sidebar-width: 250px;
}
.root {
height: 100%;
}
.app_container {
display: grid;
height: 100%;
grid-template-columns: auto 100%;
grid-template-rows: 60px auto 60px;
grid-template-areas:
"sidebar header"
"sidebar mc"
"sidebar footer";
}
.header {
grid-area: header;
background-color: var(--primary);
padding: 12px;
display: flex;
}
.header > h1 {
margin: 0;
padding: 0;
font-size: 24px;
line-height: 36px;
color: white;
margin-left: 2rem;
}
.header > button {
height: 36px;
background-color: transparent;
border: none;
font-size: 20px;
}
.header > button:hover {
background-color: rgba(255, 255, 255, 0.151);
}
.sidebar {
width: 0;
overflow: hidden;
grid-area: sidebar;
transition: width 0.2s;
background-color: lightgrey;
height: 100%;
}
.sidebar-visible {
width: var(--sidebar-width);
transition: width 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
grid-area: mc;
padding: 1rem;
}
.footer {
grid-area: footer;
}
@media (min-width: 45rem) {
.app_container {
grid-template-columns: auto 1fr;
}
.sidebar {
width: var(--sidebar-width);
transition: all 0.2s;
box-shadow: 10px 0px 10px 2px rgba(0, 0, 0, 0.52);
}
.content {
padding: 2rem;
}
}
.loader_container {
position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 2;
}
</style>

View File

@ -0,0 +1,188 @@
<script context="module">
const TFATypes = new Map();
TFATypes.set(0, "Authenticator");
TFATypes.set(1, "Backup Codes");
TFATypes.set(2, "YubiKey");
TFATypes.set(3, "Push Notification");
</script>
<script>
import Box from "./Box.svelte";
import BoxItem from "./BoxItem.svelte";
import NextIcon from "./NextIcon.svelte";
import request from "../../../helper/request.ts";
export let loading = false;
let twofactor = [];
async function deleteTFA(id) {
let res = await request(
"/api/user/twofactor/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadTwoFactor();
}
async function loadTwoFactor() {
let res = await request(
"/api/user/twofactor",
undefined,
undefined,
undefined,
true,
true
);
twofactor = res.methods;
}
let token = [];
async function revoke(id) {
let res = await request(
"/api/user/token/" + id,
undefined,
"DELETE",
undefined,
true,
true
);
loadToken();
}
async function loadToken() {
loading = true;
let res = await request(
"/api/user/token",
undefined,
undefined,
undefined,
true,
true
);
token = res.token;
loading = false;
}
loadToken();
loadTwoFactor();
</script>
<style>
.btn {
background-color: var(--primary);
margin: auto 0;
margin-left: 1rem;
font-size: 1rem;
padding: 0 0.5rem;
}
.floating {
margin-bottom: 0;
}
.input-container {
display: flex;
}
.input-container > *:first-child {
flex-grow: 1;
}
select {
background-color: unset;
border: 0;
border-radius: 0;
color: unset;
font-size: unset;
border-bottom: 1px solid #757575;
/* Firefox */
-moz-appearance: none;
/* Safari and Chrome */
-webkit-appearance: none;
appearance: none;
height: 100%;
width: 100%;
}
select > option {
background-color: unset;
}
.select-wrapper {
position: relative;
}
.select-wrapper::after {
content: ">";
display: block;
position: absolute;
right: 2rem;
top: 0;
bottom: 0;
width: 1rem;
transform: rotate(90deg) scaleY(2);
}
</style>
<Box>
<h1>Two Factor</h1>
<BoxItem name="Add new" open={false} />
{#each twofactor as t}
<BoxItem name={TFATypes.get(t.type)} value={t.name} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => deleteTFA(t.id)}>
Delete
</button>
</BoxItem>
{/each}
<!-- <BoxItem name="Name" value={name} open={false}>
<div class="input-container">
<div class="floating group">
<input type="text" autocomplete="username" bind:value={name}>
<span class="highlight"></span>
<span class="bar"></span>
<label>Name</label>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Gender" value={gender} open={true}>
<div class="input-container">
<div class="select-wrapper">
<select>
<option value="1">Male</option>
<option value="2">Female</option>
<option value="3">Other</option>
</select>
</div>
<button class="btn" on:click={saveName}>Save</button>
</div>
</BoxItem>
<BoxItem name="Birthday" value={birthday} />
<BoxItem name="Password" value="******" /> -->
</Box>
<Box>
<h1>Anmeldungen</h1>
{#each token as t}
<BoxItem name={t.browser} value={t.ip} highlight={t.isthis}>
<button
class="btn"
style="background: var(--error)"
on:click={() => revoke(t.id)}>
Revoke
</button>
</BoxItem>
{:else}<span>No Tokens</span>{/each}
<!-- <BoxItem name="E-Mail" value={email} />
<BoxItem name="Phone" value={phone} /> -->
</Box>

View File

@ -0,0 +1,6 @@
import "../../components/theme";
import App from "./App.svelte";
new App({
target: document.body,
});