A lot of new views

This commit is contained in:
Fabian Stamm
2019-08-06 17:08:07 +02:00
commit 27b41f1bec
77 changed files with 22268 additions and 0 deletions

37
src/Home/App.svelte Normal file
View File

@ -0,0 +1,37 @@
<style>
.main {
padding: 2rem;
}
li {
list-style: none;
padding: 1rem;
}
li>a {
text-decoration: none;
}
</style>
<div class="main">
<h1>Home Page</h1>
<h2>About</h2>
<p>
OpenAuth is a Service to provide simple Authentication to a veriaty of Applications.
With a simple to use API and different Strategies, it can be easily integrated
into most Applications.
</p>
<h2>QickLinks</h2>
<p>
If you want to manage your Account, click <a href="user.html">here</a>
</p>
<h2> Applications using OpenAuth </h2>
<ul>
<li><a href="https://ebook.stamm.me">EBook Store and Reader</a></li>
<li><a href="https://notes.hibas123.de">Secure and Simple Notes application</a></li>
</ul>
</div>

7
src/Home/main.js Normal file
View File

@ -0,0 +1,7 @@
import App from './App.svelte';
var app = new App({
target: document.getElementById("content")
});
export default app;

206
src/Login/App.svelte Normal file
View File

@ -0,0 +1,206 @@
<script>
import Api from "./api";
import Credentials from "./Credentials.svelte";
import Redirect from "./Redirect.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();
}
</script>
<div class="form-container">
<form action="JavaScript:void(0)" class="card">
<div class="card title-container">
<h1>Login</h1>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader"></div>
</div>
</div>
{/if}
<div class="container" class:loading_container={loading}>
{#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}
</div>
</form>
</div>
<footer>
<p>Powered by {appname}</p>
</footer>
<style>
.card {
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
background: #fafafa;
border-radius: 4px;
}
.form-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
box-sizing: border-box;
}
form {
width: 100%;
max-width: 380px;
margin: 0 auto;
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22);
position: relative;
padding: 1px;
background-color: white !important;
margin-bottom: 40px;
}
.container {
overflow: hidden;
padding: 2em;
}
button {
margin-top: 16px;
}
footer {
text-align: center;
position: absolute;
bottom: 0;
left: 0;
right: 0;
}
.floating {
margin-top: 8px;
}
h3 {
font-weight: 200;
}
.title-container {
margin: -30px auto 0 auto;
max-width: 250px;
background-color: var(--primary);
color: white;
padding: 5px 20px;
}
.loading_container {
filter: blur(1px) opacity(50%);
}
.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,79 @@
<script>
import Api from "./api";
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;
}
.btn {
background-color: var(--primary);
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>
<span class="bar"></span>
<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>
<span class="bar"></span>
<label>Password</label>
<div class="error" style={!error ? "display: none;" : "" }>{error}</div>
</div>
{/if}
<button class="btn" on:click={buttonClick}>
Next
</button>

100
src/Login/Redirect.svelte Normal file
View File

@ -0,0 +1,100 @@
<script>
// import {
// onMount,
// onDestroy
// } from "svelte";
import {
onMount,
onDestroy
} from 'svelte';
const basetext = "Logged in. Redirecting";
let dots = 0;
$: text = basetext + ".".repeat(dots);
let iv;
onMount(() => {
console.log("Mounted");
iv = setInterval(() => {
dots++;
if (dots > 3)
dots = 0;
}, 500);
});
onDestroy(() => {
console.log("on Destroy")
clearInterval(iv)
});
</script>
<div class="scale">
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none" />
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
</svg>
</div>
<!-- <div style="text-align: center;"> -->
<h3>{text}</h3>
<!-- </div> -->
<style>
.checkmark__circle {
stroke-dasharray: 166;
stroke-dashoffset: 166;
stroke-width: 2;
stroke-miterlimit: 10;
stroke: #7ac142;
fill: none;
animation: stroke 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
}
.checkmark {
width: 56px;
height: 56px;
border-radius: 50%;
display: block;
stroke-width: 2;
stroke: #fff;
stroke-miterlimit: 10;
margin: 10% auto;
box-shadow: inset 0px 0px 0px #7ac142;
animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both;
}
.checkmark__check {
transform-origin: 50% 50%;
stroke-dasharray: 48;
stroke-dashoffset: 48;
animation: stroke 0.3s cubic-bezier(0.65, 0, 0.45, 1) 0.8s forwards;
}
@keyframes stroke {
100% {
stroke-dashoffset: 0;
}
}
@keyframes scale {
0%,
100% {
transform: none;
}
50% {
transform: scale3d(1.1, 1.1, 1);
}
}
@keyframes fill {
100% {
box-shadow: inset 0px 0px 0px 30px #7ac142;
}
}
.scale {
transform: scale(1.5);
}
</style>

112
src/Login/Twofactor.svelte Normal file
View File

@ -0,0 +1,112 @@
<script>
import Api, {
TFATypes
} from "./api";
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}
{/if}
</div>

127
src/Login/api.ts Normal file
View File

@ -0,0 +1,127 @@
import request from "../request";
import sha from "../sha512";
import {
setCookie,
getCookie
} from "../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[],
getUsername() {
return this.username || getCookie("username");
},
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 }
})
},
async setPassword(password: string): Promise<{ error: string | undefined, twofactor?: any }> {
let pw = sha(this.salt + password);
return request("/api/user/login", {
type: "password"
}, "POST", {
username: this.username,
password: pw
}
).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 }
})
},
gettok() {
return {
login: this.login.token,
special: this.special.token
}
},
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 }));
},
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 }))
},
async 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
}
window.location.href = red;
}
}
export default Api;

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18.617,1.72c0,-0.949 -0.771,-1.72 -1.721,-1.72l-9.792,0c-0.95,0 -1.721,0.771 -1.721,1.72l0,20.56c0,0.949 0.771,1.72 1.721,1.72l9.792,0c0.95,0 1.721,-0.771 1.721,-1.72l0,-20.56Z" style="fill:#4d4d4d;"/><rect x="6" y="3" width="12" height="18" style="fill:#b3b3b3;"/><path d="M14,1.5c0,-0.129 -0.105,-0.233 -0.233,-0.233l-3.534,0c-0.128,0 -0.233,0.104 -0.233,0.233c0,0.129 0.105,0.233 0.233,0.233l3.534,0c0.128,0 0.233,-0.104 0.233,-0.233Z" style="fill:#b3b3b3;"/><ellipse cx="12" cy="22.5" rx="0.983" ry="1" style="fill:#b3b3b3;"/></svg>

After

Width:  |  Height:  |  Size: 992 B

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><path d="M18.5,12c0,3.59 -2.91,6.5 -6.5,6.5c-3.59,0 -6.5,-2.91 -6.5,-6.5c0,-3.59 2.91,-6.5 6.5,-6.5c1.729,0 3.295,0.679 4.46,1.78l4.169,-3.599c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5.5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle id="XMLID_1331_" cx="12" cy="12" r="12" style="fill:#808080;"/><path d="M19,12c0,3.866 -3.134,7 -7,7c-3.866,0 -7,-3.134 -7,-7c0,-3.866 3.134,-7 7,-7c1.88,0 3.583,0.745 4.841,1.951l3.788,-3.27c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle cx="12" cy="2.5" r="1" style="fill:#b3b3b3;"/><circle cx="12" cy="21.5" r="1" style="fill:#b3b3b3;"/><circle cx="2.5" cy="12" r="1" style="fill:#b3b3b3;"/><path d="M4.575,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M18.01,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M4.575,4.575c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><circle id="XMLID_1329_" cx="12" cy="12" r="6" style="fill:#808080;"/><circle id="XMLID_1330_" cx="12" cy="12" r="7" style="fill:#808080;"/><path d="M19,12.25c0,-0.042 -0.006,-0.083 -0.006,-0.125c-0.068,3.808 -3.17,6.875 -6.994,6.875c-3.824,0 -6.933,-3.067 -7,-6.875c-0.001,0.042 0,0.083 0,0.125c0,3.866 3.134,7 7,7c3.866,0 7,-3.134 7,-7Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M18.92,13l-3.061,0c0.083,-0.321 0.141,-0.653 0.141,-1c0,-2.209 -1.791,-4 -4,-4c-2.209,0 -4,1.791 -4,4c0,1.105 0.448,2.105 1.172,2.828c1.014,1.015 4.057,4.058 4.057,4.058c2.955,-0.525 5.263,-2.899 5.691,-5.886Z" style="fill:#4d4d4d;fill-rule:nonzero;"/><path d="M22,13l-10,0c-0.553,0 -1,-0.448 -1,-1c0,-0.552 0.447,-1 1,-1l10,0c0.553,0 1,0.448 1,1c0,0.552 -0.447,1 -1,1Z" style="fill:#b3b3b3;fill-rule:nonzero;"/><path d="M11.948,11.25l10.104,0c0.409,0 0.776,0.247 0.935,0.592c-0.08,-0.471 -0.492,-0.842 -0.987,-0.842l-10,0c-0.495,0 -0.9,0.33 -0.98,0.801c0.159,-0.345 0.519,-0.551 0.928,-0.551Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M23,12c0,0.552 -0.447,1 -1,1l-3.08,0c-0.428,2.988 -2.737,5.362 -5.693,5.886l3.935,3.946c4.04,-1.931 6.838,-6.056 6.838,-10.832l-1,0Z" style="fill:#666;fill-opacity:0.5;fill-rule:nonzero;"/><path d="M12,5c-3.866,0 -7,3.134 -7,7c0,0.042 -0.001,0.069 0,0.111c0.067,-3.808 3.176,-6.861 7,-6.861c2.828,0 4.841,1.701 4.841,1.701c-1.257,-1.198 -2.968,-1.951 -4.841,-1.951Z" style="fill-opacity:0.1;fill-rule:nonzero;"/><circle id="XMLID_4_" cx="12" cy="12" r="12" style="fill:url(#_Linear1);"/></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(21.7566,10.1453,-10.1453,21.7566,1.12171,6.92737)"><stop offset="0" style="stop-color:#fff;stop-opacity:0.2"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><path d="M20.562,9.105c0,-0.853 -0.692,-1.544 -1.544,-1.544l-14.036,0c-0.852,0 -1.544,0.691 -1.544,1.544l0,12.351c0,0.852 0.692,1.544 1.544,1.544l14.036,0c0.852,0 1.544,-0.692 1.544,-1.544l0,-12.351Z" style="fill:none;stroke:#000;stroke-width:1.5px;"/><circle cx="12" cy="15.3" r="1.5"/><path d="M16.646,4.28c0,-1.81 -1.47,-3.28 -3.28,-3.28l-2.732,0c-1.81,0 -3.28,1.47 -3.28,3.28l0,3.281l9.292,0l0,-3.281Z" style="fill:none;stroke:#000;stroke-width:1.5px;"/></svg>

After

Width:  |  Height:  |  Size: 927 B

View File

@ -0,0 +1,15 @@
<script>
export let icon_name;
</script>
{#if icon_name === "SecurityKey"}
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18,7.692c0,-0.925 -0.751,-1.675 -1.675,-1.675l-14.65,0c-0.924,0 -1.675,0.75 -1.675,1.675l0,8.616c0,0.925 0.751,1.675 1.675,1.675l14.65,0c0.924,0 1.675,-0.75 1.675,-1.675l0,-8.616Z" style="fill:#4d4d4d;"/><rect x="18" y="8.011" width="6" height="7.978" style="fill:#4d4d4d;"/><rect x="18" y="10.644" width="4.8" height="1.231" style="fill:#b3b3b3;"/><rect x="18" y="12.229" width="4.8" height="1.164" style="fill:#b3b3b3;"/><rect x="18" y="9.008" width="5.25" height="1.231" style="fill:#b3b3b3;"/><rect x="18" y="13.794" width="5.25" height="1.197" style="fill:#b3b3b3;"/></svg>
{:else if icon_name === "Authenticator"}
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><g><path d="M18.5,12c0,3.59 -2.91,6.5 -6.5,6.5c-3.59,0 -6.5,-2.91 -6.5,-6.5c0,-3.59 2.91,-6.5 6.5,-6.5c1.729,0 3.295,0.679 4.46,1.78l4.169,-3.599c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5.5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle id="XMLID_1331_" cx="12" cy="12" r="12" style="fill:#808080;"/><path d="M19,12c0,3.866 -3.134,7 -7,7c-3.866,0 -7,-3.134 -7,-7c0,-3.866 3.134,-7 7,-7c1.88,0 3.583,0.745 4.841,1.951l3.788,-3.27c-2.184,-2.265 -5.242,-3.681 -8.629,-3.681c-6.617,0 -12,5.383 -12,12c0,6.617 5.383,12 12,12c6.617,0 12,-5.383 12,-12l-5,0Z" style="fill:#999;fill-rule:nonzero;"/><circle cx="12" cy="2.5" r="1" style="fill:#b3b3b3;"/><circle cx="12" cy="21.5" r="1" style="fill:#b3b3b3;"/><circle cx="2.5" cy="12" r="1" style="fill:#b3b3b3;"/><path d="M4.575,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M18.01,18.01c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><path d="M4.575,4.575c0.391,-0.39 1.024,-0.39 1.415,0c0.39,0.391 0.39,1.024 0,1.415c-0.391,0.39 -1.024,0.39 -1.415,0c-0.39,-0.391 -0.39,-1.024 0,-1.415Z" style="fill:#b3b3b3;"/><circle id="XMLID_1329_" cx="12" cy="12" r="6" style="fill:#808080;"/><circle id="XMLID_1330_" cx="12" cy="12" r="7" style="fill:#808080;"/><path d="M19,12.25c0,-0.042 -0.006,-0.083 -0.006,-0.125c-0.068,3.808 -3.17,6.875 -6.994,6.875c-3.824,0 -6.933,-3.067 -7,-6.875c-0.001,0.042 0,0.083 0,0.125c0,3.866 3.134,7 7,7c3.866,0 7,-3.134 7,-7Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M18.92,13l-3.061,0c0.083,-0.321 0.141,-0.653 0.141,-1c0,-2.209 -1.791,-4 -4,-4c-2.209,0 -4,1.791 -4,4c0,1.105 0.448,2.105 1.172,2.828c1.014,1.015 4.057,4.058 4.057,4.058c2.955,-0.525 5.263,-2.899 5.691,-5.886Z" style="fill:#4d4d4d;fill-rule:nonzero;"/><path d="M22,13l-10,0c-0.553,0 -1,-0.448 -1,-1c0,-0.552 0.447,-1 1,-1l10,0c0.553,0 1,0.448 1,1c0,0.552 -0.447,1 -1,1Z" style="fill:#b3b3b3;fill-rule:nonzero;"/><path d="M11.948,11.25l10.104,0c0.409,0 0.776,0.247 0.935,0.592c-0.08,-0.471 -0.492,-0.842 -0.987,-0.842l-10,0c-0.495,0 -0.9,0.33 -0.98,0.801c0.159,-0.345 0.519,-0.551 0.928,-0.551Z" style="fill:#fff;fill-opacity:0.2;fill-rule:nonzero;"/><path d="M23,12c0,0.552 -0.447,1 -1,1l-3.08,0c-0.428,2.988 -2.737,5.362 -5.693,5.886l3.935,3.946c4.04,-1.931 6.838,-6.056 6.838,-10.832l-1,0Z" style="fill:#666;fill-opacity:0.5;fill-rule:nonzero;"/><path d="M12,5c-3.866,0 -7,3.134 -7,7c0,0.042 -0.001,0.069 0,0.111c0.067,-3.808 3.176,-6.861 7,-6.861c2.828,0 4.841,1.701 4.841,1.701c-1.257,-1.198 -2.968,-1.951 -4.841,-1.951Z" style="fill-opacity:0.1;fill-rule:nonzero;"/><circle id="XMLID_4_" cx="12" cy="12" r="12" style="fill:url(#_Linear1);"/></g><defs><linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(21.7566,10.1453,-10.1453,21.7566,1.12171,6.92737)"><stop offset="0" style="stop-color:#fff;stop-opacity:0.2"/><stop offset="1" style="stop-color:#fff;stop-opacity:0"/></linearGradient></defs></svg>
{:else if icon_name === "BackupCode"}
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;"><path d="M20.562,9.105c0,-0.853 -0.692,-1.544 -1.544,-1.544l-14.036,0c-0.852,0 -1.544,0.691 -1.544,1.544l0,12.351c0,0.852 0.692,1.544 1.544,1.544l14.036,0c0.852,0 1.544,-0.692 1.544,-1.544l0,-12.351Z" style="fill:none;stroke:#000;stroke-width:1.5px;"/><circle cx="12" cy="15.3" r="1.5"/><path d="M16.646,4.28c0,-1.81 -1.47,-3.28 -3.28,-3.28l-2.732,0c-1.81,0 -3.28,1.47 -3.28,3.28l0,3.281l9.292,0l0,-3.281Z" style="fill:none;stroke:#000;stroke-width:1.5px;"/></svg>
{:else if icon_name === "AppPush"}
<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18.617,1.72c0,-0.949 -0.771,-1.72 -1.721,-1.72l-9.792,0c-0.95,0 -1.721,0.771 -1.721,1.72l0,20.56c0,0.949 0.771,1.72 1.721,1.72l9.792,0c0.95,0 1.721,-0.771 1.721,-1.72l0,-20.56Z" style="fill:#4d4d4d;"/><rect x="6" y="3" width="12" height="18" style="fill:#b3b3b3;"/><path d="M14,1.5c0,-0.129 -0.105,-0.233 -0.233,-0.233l-3.534,0c-0.128,0 -0.233,0.104 -0.233,0.233c0,0.129 0.105,0.233 0.233,0.233l3.534,0c0.128,0 0.233,-0.104 0.233,-0.233Z" style="fill:#b3b3b3;"/><ellipse cx="12" cy="22.5" rx="0.983" ry="1" style="fill:#b3b3b3;"/></svg>
{:else}
ERR
{/if}

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;"><path d="M18,7.692c0,-0.925 -0.751,-1.675 -1.675,-1.675l-14.65,0c-0.924,0 -1.675,0.75 -1.675,1.675l0,8.616c0,0.925 0.751,1.675 1.675,1.675l14.65,0c0.924,0 1.675,-0.75 1.675,-1.675l0,-8.616Z" style="fill:#4d4d4d;"/><rect x="18" y="8.011" width="6" height="7.978" style="fill:#4d4d4d;"/><rect x="18" y="10.644" width="4.8" height="1.231" style="fill:#b3b3b3;"/><rect x="18" y="12.229" width="4.8" height="1.164" style="fill:#b3b3b3;"/><rect x="18" y="9.008" width="5.25" height="1.231" style="fill:#b3b3b3;"/><rect x="18" y="13.794" width="5.25" height="1.197" style="fill:#b3b3b3;"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

7
src/Login/main.js Normal file
View File

@ -0,0 +1,7 @@
import App from './App.svelte';
var app = new App({
target: document.getElementById("content")
});
export default app;

View File

@ -0,0 +1,35 @@
<script>
import Cleave from "../../cleave"
import {
onMount
} from 'svelte';
export let error;
export let label;
export let value;
export let length = 6;
let input;
onMount(() => {
const cleaveCustom = new Cleave(input, {
blocks: [length / 2, length / 2],
delimiter: ' ',
numericOnly: true
});
})
</script>
<style>
.error {
color: var(--error);
margin-top: 4px;
}
</style>
<div class="floating group">
<input bind:this={input} autofocus bind:value={value}>
<span class="highlight"></span>
<span class="bar"></span>
<label>Code</label>
<div class="error" style={!error ? "display: none;" : "" }>{error}</div>
</div>

View File

@ -0,0 +1,56 @@
<script>
import ToList from "./toList.svelte";
import Api from "../api";
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 {
background-color: var(--primary);
margin: 0;
margin-left: auto;
min-width: 80px;
}
</style>
<h3>{title}</h3>
<CodeInput bind:value={code} label="Code" error={error} length={length} />
<div class="actions">
<ToList {finish} />
<button class="btn" style="margin-left: auto" on:click={sendCode}>Send</button>
</div>

View File

@ -0,0 +1,393 @@
<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>
<div class="wBall" id="wBall_2">
<div class="wInnerBall"></div>
</div>
<div class="wBall" id="wBall_3">
<div class="wInnerBall"></div>
</div>
<div class="wBall" id="wBall_4">
<div class="wInnerBall"></div>
</div>
<div class="wBall" id="wBall_5">
<div class="wInnerBall"></div>
</div>
</div>
<div class="error">{error}</div>
<ToList {finish} />

View File

@ -0,0 +1,49 @@
<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" 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 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}/>

View File

@ -0,0 +1,13 @@
<script>
export let finish = () => {};
</script>
<style>
a {
color: var(--primary);
text-decoration: none;
}
</style>
<p>
<a href="# " on:click={evt=>evt.preventDefault() || finish(false)}>Choose another Method</a>
</p>

View File

@ -0,0 +1,72 @@
<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} />

245
src/Public/global.css Normal file
View File

@ -0,0 +1,245 @@
:root {
--primary: #1E88E5;
--mdc-theme-primary: var(--primary);
--mdc-theme-primary-bg: var(--mdc-theme--primary);
--mdc-theme-on-primary: white;
--error: #ff2f00;
--border-color: #ababab;
--default-font-size: 1.05rem;
}
* {
font-family: 'Roboto', 'Helvetica', sans-serif;
}
html,
body {
margin: 0;
color: #636363;
position: relative;
background: #eee;
height: 100%;
font-size: var(--default-font-size);
}
.group {
position: relative;
margin-bottom: 24px;
min-height: 45px;
}
.floating>input {
font-size: 1.2rem;
padding: 10px 10px 10px 5px;
appearance: none;
-webkit-appearance: none;
display: block;
background: #fafafa;
background: unset;
color: #636363;
width: 100%;
border: none;
border-radius: 0;
/* border-bottom: 1px solid #757575; */
border-bottom: 1px solid var(--border-color);
box-sizing: border-box;
}
.floating>input:focus {
outline: none;
}
/* Label */
.floating>label {
color: #999;
font-size: 18px;
font-weight: normal;
position: absolute;
pointer-events: none;
left: 5px;
top: 10px;
transition: all 0.2s ease;
}
/* active */
.floating>input:focus~label,
.floating>input.used~label {
top: -.75em;
transform: scale(.75);
left: -2px;
/* font-size: 14px; */
color: var(--primary);
transform-origin: left;
}
/* Underline */
.bar {
position: relative;
display: block;
width: 100%;
}
.bar:before,
.bar:after {
content: '';
height: 2px;
width: 0;
bottom: 1px;
position: absolute;
background: var(--primary);
transition: all 0.2s ease;
}
.bar:before {
left: 50%;
}
.bar:after {
right: 50%;
}
/* active */
.floating>input:focus~.bar:before,
.floating>input:focus~.bar:after {
width: 50%;
}
/* Highlight */
.highlight {
position: absolute;
height: 60%;
width: 100px;
top: 25%;
left: 0;
pointer-events: none;
opacity: 0.5;
}
/* active */
.floating>input:focus~.highlight {
animation: inputHighlighter 0.3s ease;
}
/* Animations */
@keyframes inputHighlighter {
from {
background: var(--primary);
}
to {
width: 0;
background: transparent;
}
}
.btn {
position: relative;
display: block;
margin: 2rem;
padding: 0 1em;
overflow: hidden;
border-width: 0;
outline: none;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
background-color: #cccccc;
color: #ecf0f1;
transition: background-color .3s;
height: 48px;
text-transform: uppercase;
font-weight: 500;
font-size: 1.2rem;
}
.btn:hover,
.btn:focus {
filter: brightness(90%);
}
.btn>* {
position: relative;
}
.btn span {
display: block;
padding: 12px 24px;
}
.btn:before {
content: "";
position: absolute;
top: 50%;
left: 50%;
display: block;
width: 0;
padding-top: 0;
border-radius: 100%;
background-color: rgba(236, 240, 241, .3);
-webkit-transform: translate(-50%, -50%);
-moz-transform: translate(-50%, -50%);
-ms-transform: translate(-50%, -50%);
-o-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.btn:active:before {
width: 120%;
padding-top: 120%;
transition: width .2s ease-out, padding-top .2s ease-out;
}
.loader_box {
width: 64px;
height: 64px;
margin: auto;
}
.loader {
display: inline-block;
position: relative;
z-index: 100;
}
.loader:after {
content: " ";
display: block;
width: 46px;
height: 46px;
margin: 1px;
border-radius: 50%;
border: 5px solid var(--primary);
border-color: var(--primary) transparent var(--primary) transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

36
src/Public/main.js Normal file
View File

@ -0,0 +1,36 @@
// import "./global.css";
(() => {
const elements = new WeakSet();
function check() {
document.querySelectorAll(".floating>input").forEach(e => {
if (elements.has(e)) return;
elements.add(e);
function checkState() {
console.log("Check State");
if (e.value !== "") {
if (e.classList.contains("used")) return;
e.classList.add("used")
} else {
if (e.classList.contains("used")) e.classList.remove("used")
}
}
e.addEventListener("change", () => checkState())
checkState()
})
};
const observer = new MutationObserver((mutations) => {
check();
});
// Start observing the target node for configured mutations
observer.observe(window.document, {
childList: true,
subtree: true
});
check();
})()

195
src/User/App.svelte Normal file
View File

@ -0,0 +1,195 @@
<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: "",
component: AccountPage
},
{
id: "security",
title: "Security",
icon: "",
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="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={pages} />
</div>
<div class="content">
<svelte:component this={page.component} bind:loading />
</div>
<div class="footer"></div>
</div>
</div>
{#if loading}
<div class="loader_container">
<div class="loader_box">
<div class="loader"></div>
</div>
</div>
{/if}
<style>
.loading {
background-color: rgba(0, 0, 0, 0.04);
filter: blur(10px);
}
:root {
--sidebar-width: 250px;
}
.root {
height: 100%;
}
.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 .2s;
background-color: lightgrey;
height: 100%;
}
.sidebar-visible {
width: var(--sidebar-width);
transition: width .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) {
.container {
grid-template-columns: auto 1fr;
}
.sidebar {
width: var(--sidebar-width);
transition: all .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,46 @@
<script>
export let open;
export let pages = []
</script>
<style>
:root {
--rel-size: 0.75rem;
}
.container {
height: calc(var(--rel-size) * 3);
padding: var(--rel-size);
display: flex;
/* align-content: center; */
align-items: center;
/* justify-content: center; */
}
.icon {
/* float: left; */
width: calc(var(--rel-size) * 3);
height: calc(var(--rel-size) * 3);
}
.icon>img {
width: calc(var(--rel-size) * 3);
height: calc(var(--rel-size) * 3);
}
.title {
/* margin: auto; */
margin-left: var(--rel-size);
/* height: 100%; */
}
</style>
{#each pages as page}
<div class="container" on:click={() => open(page.id)}>
<div class="icon"><img src={page.icon} /></div>
<h3 class="title">
{page.title}
</h3>
</div>
{/each}

View File

@ -0,0 +1,166 @@
<script>
import Box from "./Box.svelte";
import BoxItem from "./BoxItem.svelte";
import NextIcon from "./NextIcon.svelte";
import request from "../../request"
export let loading = false;
let error = undefined;
const genderMap = new Map();
genderMap.set(0, "None");
genderMap.set(1, "Male");
genderMap.set(2, "Female");
genderMap.set(3, "Other");
let name = "";
let gender = 0;
$: genderHuman = genderMap.get(gender) || "ERROR";
let birthday = undefined;
async function saveName() {
//TODO: implement
await load();
}
async function saveGender() {
//TODO: implement
await load();
}
async function loadProfile() {
try {
let {
user
} = await request("/api/user/account", {}, "GET", undefined, true)
name = user.name;
// username = user.username;
gender = user.gender;
birthday = user.birthday ? new Date(user.birthday).toLocaleDateString() : undefined;
} catch (err) {
console.error(err);
error = err.message;
}
}
let email = ["mail@fabianstamm.de", "fabian.stamm.koe@gmail.com"];
let phone = ["+1 1233 123123123", "+21 1233 123 123 1"];
async function loadContact() {
}
async function load() {
loading = true;
await Promise.all([
loadProfile(),
loadContact()
])
loading = false;
}
load();
</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);
}
.error {
color: var(--error);
}
</style>
<Box>
<h1>Profile</h1>
{#if error}
<p class="error">{error}</p>
{/if}
<BoxItem name="Name" value={name}>
<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={genderHuman}>
<div class="input-container">
<div class="select-wrapper">
<select bind:value={gender}>
<option value={1}>Male</option>
<option value={2}>Female</option>
<option value={3}>Other</option>
</select>
</div>
<button class="btn" on:click={saveGender}>Save</button>
</div>
</BoxItem>
<BoxItem name="Birthday" value={birthday} />
<BoxItem name="Password" value="******" />
</Box>
<Box>
<h1>Contact</h1>
<BoxItem name="E-Mail" value={email} />
<BoxItem name="Phone" value={phone} />
</Box>

36
src/User/Pages/Box.svelte Normal file
View File

@ -0,0 +1,36 @@
<style>
.box {
border-radius: 4px;
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.30), 0 5px 4px rgba(0, 0, 0, 0.22);
padding: 2rem;
margin-bottom: 1rem;
background-color: white;
}
.box> :global(h1) {
margin: 0;
margin-bottom: 1rem;
color: #444444;
font-size: 1.3rem;
}
.box> :global(div) {
padding: 16px;
border-top: 1px solid var(--border-color);
word-wrap: break-word;
}
.box> :global(div):first-of-type {
border-top: none;
}
@media (min-width: 45rem) {
.box {
margin-bottom: 2rem;
}
}
</style>
<div class="box">
<slot></slot>
</div>

View File

@ -0,0 +1,97 @@
<script>
import {
slide
} from 'svelte/transition';
import NextIcon from "./NextIcon.svelte"
export let name = "";
export let value = "";
export let open = false;
export let highlight = false;
function toggleOpen(ev) {
}
</script>
<style>
.root:hover {
background-color: rgba(0, 0, 0, 0.04);
}
.container {
display: flex;
flex-direction: row;
}
.values {
flex-grow: 1;
display: flex;
flex-direction: column;
max-width: calc(100% - var(--default-font-size) - 16px);
}
.values>div:first-child {
transform-origin: left;
transform: scale(0.95);
margin-right: 24px;
font-weight: 500;
}
.values>div:nth-child(2) {
color: black;
}
:global(svg) {
margin: auto 8px auto 8px;
height: var(--default-font-size);
min-width: var(--default-font-size);
}
.body {
box-sizing: border-box;
padding: .1px;
margin-top: 2rem;
}
@media (min-width: 45rem) {
.values {
flex-direction: row;
}
.values>div:first-child {
transform: unset;
flex-basis: 120px;
min-width: 120px;
}
}
.highlight-element {
background-color: #7bff003b;
}
</style>
<div class="root" class:highlight-element={highlight}>
<div class="container" on:click={()=>open=!open}>
<div class="values">
<div>{name}</div>
<div>
{#if Array.isArray(value)}
{#each value as v, i}
{v}
{#if i < value.length - 1}
<br/>
{/if}
{/each}
{:else}
{value}
{/if}
</div>
</div>
<NextIcon rotation={open ? -90 : 90} />
</div>
{#if open}
<div class="body" transition:slide>
<slot></slot>
</div>
{/if}
</div>

View File

@ -0,0 +1,13 @@
<script>
export let rotation;
</script>
<svg style={`enable-background:new 0 0 35.414 35.414; transform: rotate(${rotation}deg); transition: all .4s;`}
version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
y="0px" viewBox="0 0 35.414 35.414" xml:space="preserve">
<g>
<g>
<polygon points="27.051,17 9.905,0 8.417,1.414 24.674,17.707 8.363,34 9.914,35.414 27.051,18.414 " />
</g>
</g>
</svg>

View File

@ -0,0 +1,155 @@
<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 "../../request";
export let loading = false;
let twofactor = [];
async function deleteTFA(id) {
let res = await request("/api/user/twofactor/" + id, undefined, "DELETE", undefined, true);
loadTwoFactor();
}
async function loadTwoFactor() {
let res = await request("/api/user/twofactor", undefined, undefined, undefined, true);
twofactor = res.methods;
}
let token = [];
async function revoke(id) {
let res = await request("/api/user/token/" + id, undefined, "DELETE", undefined, true);
loadToken();
}
async function loadToken() {
loading = true;
let res = await request("/api/user/token", undefined, undefined, undefined, true);
token = res.token;
loading = false;
}
loadToken();
loadTwoFactor();
</script>
<Box>
<h1>Two Factor</h1>
<BoxItem name="Add new" open={false}></BoxItem>
{#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>
<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>

7
src/User/main.js Normal file
View File

@ -0,0 +1,7 @@
import App from './App.svelte';
var app = new App({
target: document.getElementById("content")
});
export default app;

1515
src/cleave.js Normal file

File diff suppressed because it is too large Load Diff

20
src/cookie.ts Normal file
View File

@ -0,0 +1,20 @@
export function setCookie(cname: string, cvalue: string, exdate: string) {
const expires = exdate ? `;expires=${exdate}` : "";
document.cookie = `${cname}=${cvalue}${expires}`
}
export function getCookie(cname: string) {
const name = cname + "=";
const dc = decodeURIComponent(document.cookie);
const ca = dc.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}

43
src/request.ts Normal file
View File

@ -0,0 +1,43 @@
import { getCookie } from "./cookie";
// const baseURL = "https://auth.stamm.me";
const baseURL = "http://localhost:3000";
export default async function request(endpoint: string, parameters: { [key: string]: string } = {}, method: "GET" | "POST" | "DELETE" | "PUT" = "GET", body?: any, authInParam = false) {
let pairs = [];
if (authInParam) {
parameters.login = getCookie("login");
parameters.special = getCookie("special");
}
for (let key in parameters) {
pairs.push(key + "=" + parameters[key]);
}
let url = endpoint;
if (pairs.length > 0) {
url += "?" + pairs.join("&");
}
return fetch(baseURL + url, {
method,
body: JSON.stringify(body),
credentials: "same-origin",
headers: {
'content-type': 'application/json'
},
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
if (data.additional && data.additional.auth) {
let state = btoa(window.location.pathname + window.location.hash);
// window.location.href = `/login?state=${state}&base64=true`;
}
return Promise.reject(new Error(data.error))
}
return data;
})
}

1
src/sha512.js Normal file

File diff suppressed because one or more lines are too long

6
src/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"module": "esnext",
"sourceMap": true
}
}