First version of OpenAuth remake

This commit is contained in:
Fabian Stamm
2018-11-06 20:48:50 +01:00
commit ac69e73344
89 changed files with 14355 additions and 0 deletions

244
views/src/admin/admin.hbs Normal file
View File

@ -0,0 +1,244 @@
<html>
<head>
<title>{{i18n "Administration"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" type="text/javascript"></script>
<script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js" type="text/javascript"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js" type="text/javascript"></script>
<script>
$(document).ready(() => $('body').bootstrapMaterialDesign())
</script>
</head>
<body>
<header class=bg-primary style="display: flex; justify-content: space-between;">
<h3 style="display: inline">{{appname}} {{i18n "Administration"}}
<span id="sitename">LOADING</span>
</h3>
<ul class="nav nav-tabs" style="display: inline-block; margin-left: auto; margin-top: -8px;">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true"
aria-expanded="false">Model</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="?type=user">User</a>
<a class="dropdown-item" href="?type=regcode">RegCode</a>
<a class="dropdown-item" href="?type=client">Client</a>
</div>
</li>
</ul>
</header>
<div id=content>
<div class=container>
<div id=error_cont class=row style="margin-bottom: 24px;display: none;">
<div class=col-sm>
<div class="card error_card">
<div id="error_msg" class="card-body"></div>
</div>
</div>
</div>
<div id=custom_data_cont class=row style="margin-bottom: 24px; display: none">
<div class=col-sm>
<div class=card>
<div id=custom_data class="card-body"></div>
</div>
</div>
</div>
<div class=row>
<div class=col-sm>
<div class=card>
<div class=card-body>
<div id=table-body>
<div style="width: 65px; height: 65px; margin: 0 auto;">
<svg class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="circle" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33"
r="30"></circle>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script id="template-spinner" type="text/x-handlebars-template">
<div style="width: 65px; height: 65px; margin: 0 auto;">
<svg class="spinner" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg">
<circle class="circle" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33" r="30"></circle>
</svg>
</div>
</script>
<script id="template-user-list" type="text/x-handlebars-template">
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Name</th>
<th scope="col">Gender</th>
<th scope="col">Role</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#users}}
<tr>
<td>\{{ username }}</td>
<td>\{{ name }}</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td>\{{humangender gender}}</td>
<td onclick="userOnChangeType('\{{_id}}')">
\{{#if admin}}
<span class="badge badge-danger">Admin</span>
\{{else}}
<span class="badge badge-success">User</span>
\{{/if}}
</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteUser('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/users}}
</tbody>
</table>
</script>
<script id="template-regcode-list" type="text/x-handlebars-template">
<button class="btn btn-raised btn-primary" onclick="createRegcode()">Create</button>
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">Code</th>
<th scope="col">Valid</th>
<th scope="col">Till</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#regcodes}}
<tr>
<td>\{{ token }}</td>
<td>\{{ valid }}</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td>\{{formatDate validTill }}</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteRegcode('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/regcodes}}
</tbody>
</table>
</script>
<script id="template-client-list" type="text/x-handlebars-template">
<button class="btn btn-raised btn-primary" onclick="createClient()">Create</button>
<table class="table table-bordered" style="margin-bottom: 0">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Secret</th>
<th scope="col">Maintainer</th>
<th scope="col">Name</th>
<th scope="col" style="width: 80px">Type</th>
<th scope="col">Website</th>
<th scope="col" style="width: 2.5rem">
<div></div>
</th>
<th scope="col" style="width: 2.5rem"></th>
</tr>
</thead>
<tbody>
\{{#clients}}
<tr>
<td>\{{ client_id }}</td>
<td>\{{ client_secret }}</td>
<td>\{{ maintainer.username }}</td>
<td>\{{ name }}</td>
<td>
\{{#if internal}}
<span class="badge badge-success">Internal</span>
\{{else}}
<span class="badge badge-danger">External</span>
\{{/if}}
</td>
<td>
<a href="\{{ website }}">\{{ website }}</a>
</td>
<!-- ToDo: Make helper to resolve number to human readaby text-->
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="editClient('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
edit
</i>
</button>
</td>
<td style="padding: 0.25em">
<button style="border: 0; background-color: rgba(0, 0, 0, 0); padding: 0; text-align: center;" onclick="deleteClient('\{{_id}}')">
<i class="material-icons" style="font-size: 2rem; display: inline">
delete
</i>
</button>
</td>
</tr>
\{{/clients}}
</tbody>
</table>
</script>
<script id="template-client-form" type="text/x-handlebars-template">
<form class="form" action="JavaScript:void(null)" onsubmit="createClientSubmit(this)" style="margin-bottom: 0">
<input type=hidden value="\{{_id}}" name=id />
<div class="form-group">
<label for="name_input" class="bmd-label-floating">Name</label>
<input type="text" class="form-control" id="name_input" name=name value="\{{name}}">
</div>
<div class="form-group">
<label for="redirect_input" class="bmd-label-floating">Redirect Url</label>
<input type="text" class="form-control" id="redirect_input" name=redirect_url value="\{{redirect_url}}">
</div>
<div class="form-group">
<label for="website_input" class="bmd-label-floating">Website</label>
<input type="text" class="form-control" id="website_input" name=website value="\{{website}}">
</div>
<div class="form-group">
<label for="logo_input" class="bmd-label-floating">Logo</label>
<input type="text" class="form-control" id="logo_input" name=logo value="\{{logo}}">
</div>
<div class="form-group">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="internal_check" \{{#if internal}} checked="checked" \{{/if}} name=internal>
<label class="form-check-label" for="internal_check">Internal</label>
</div>
</div>
<span class="form-group bmd-form-group">
<!-- needed to match padding for floating labels -->
<button type="submit" class="btn btn-raised btn-primary">Save</button>
</span>
</form>
</script>
</body>
</html>

158
views/src/admin/admin.js Normal file
View File

@ -0,0 +1,158 @@
import request from "../../shared/request";
import getFormData from "../../shared/formdata";
Handlebars.registerHelper("humangender", function (value, options) {
switch (value) {
case 1:
return "male";
case 2:
return "female";
case 3:
return "other";
default:
case 0:
return "none";
}
});
// Deprecated since version 0.8.0
Handlebars.registerHelper("formatDate", function (datetime, format) {
return new Date(datetime).toLocaleString();
});
(() => {
const tableb = document.getElementById("table-body");
function setTitle(title) {
document.getElementById("sitename").innerText = title;
}
const cc = document.getElementById("custom_data")
const ccc = document.getElementById("custom_data_cont")
function setCustomCard(content) {
if (!content) {
cc.innerHTML = "";
ccc.style.display = "none";
} else {
cc.innerHTML = content;
ccc.style.display = "";
}
}
const error_cont = document.getElementById("error_cont")
const error_msg = document.getElementById("error_msg")
function catchError(error) {
error_cont.style.display = "";
error_msg.innerText = error.message;
console.log(error);
}
async function renderUser() {
console.log("Rendering User")
setTitle("User")
const listt = Handlebars.compile(document.getElementById("template-user-list").innerText)
async function loadList() {
let data = await request("/api/admin/user", "GET");
tableb.innerHTML = listt({
users: data
})
}
window.userOnChangeType = (id) => {
request("/api/admin/user?id=" + id, "PUT").then(() => loadList()).catch(catchError)
}
window.deleteUser = (id) => {
request("/api/admin/user?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
await loadList();
}
async function renderClient() {
console.log("Rendering Client")
setTitle("Client")
const listt = Handlebars.compile(document.getElementById("template-client-list").innerText)
const formt = Handlebars.compile(document.getElementById("template-client-form").innerText)
let clients = [];
async function loadList() {
let data = await request("/api/admin/client", "GET");
clients = data;
tableb.innerHTML = listt({
clients: data
})
}
window.deleteClient = (id) => {
request("/api/admin/client?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
window.createClientSubmit = (elm) => {
console.log(elm);
let data = getFormData(elm);
console.log(data);
let id = data.id;
delete data.id;
if (id !== "") {
request("/api/admin/client?id=" + id, "PUT", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError)
} else {
request("/api/admin/client", "POST", data).then(() => setCustomCard()).then(() => loadList()).catch(catchError)
}
}
window.createClient = () => {
setCustomCard(formt());
}
window.editClient = (id) => {
let client = clients.find(e => e._id === id);
if (!client) return catchError(new Error("Client does not exist!!"))
setCustomCard(formt(client));
}
await loadList();
}
async function renderRegCode() {
console.log("Rendering RegCode")
setTitle("RegCode")
const listt = Handlebars.compile(document.getElementById("template-regcode-list").innerText)
async function loadList() {
let data = await request("/api/admin/regcode", "GET");
tableb.innerHTML = listt({
regcodes: data
})
}
window.deleteRegcode = (id) => {
request("/api/admin/regcode?id=" + id, "DELETE").then(() => loadList()).catch(catchError)
}
window.createRegcode = () => {
request("/api/admin/regcode", "POST").then(() => loadList()).catch(catchError);
}
await loadList();
}
const type = new URL(window.location.href).searchParams.get("type");
switch (type) {
case "client":
renderClient().catch(catchError)
break
case "regcode":
renderRegCode().catch(catchError)
break;
case "user":
default:
renderUser().catch(catchError);
break;
}
})()

103
views/src/admin/admin.scss Normal file
View File

@ -0,0 +1,103 @@
@import "mat_bs";
@import "style";
.error_card {
color: $error;
padding: 1rem;
font-size: 1rem;
}
.bg-primary {
background-color: $primary !important;
}
.spinner {
-webkit-animation: rotation 1.35s linear infinite;
animation: rotation 1.35s linear infinite;
stroke: $primary;
}
@-webkit-keyframes rotation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
}
@keyframes rotation {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(270deg);
transform: rotate(270deg);
}
}
.circle {
stroke-dasharray: 180;
stroke-dashoffset: 0;
-webkit-transform-origin: center;
-ms-transform-origin: center;
transform-origin: center;
-webkit-animation: turn 1.35s ease-in-out infinite;
animation: turn 1.35s ease-in-out infinite;
}
@-webkit-keyframes turn {
0% {
stroke-dashoffset: 180;
}
50% {
stroke-dashoffset: 45;
-webkit-transform: rotate(135deg);
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 180;
-webkit-transform: rotate(450deg);
transform: rotate(450deg);
}
}
@keyframes turn {
0% {
stroke-dashoffset: 180;
}
50% {
stroke-dashoffset: 45;
-webkit-transform: rotate(135deg);
transform: rotate(135deg);
}
100% {
stroke-dashoffset: 180;
-webkit-transform: rotate(450deg);
transform: rotate(450deg);
}
}
header {
/* height: 60px; */
margin-bottom: 8px;
padding: 8px 16px;
padding-bottom: 0;
}
table {
word-wrap: break-word;
table-layout: fixed;
}
table td {
vertical-align: inherit !important;
width: auto;
}
.col.form-group {
padding-left: 0 !important;
margin-left: 5px !important;
}

View File

@ -0,0 +1,51 @@
<html>
<head>
<title>{{title}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<div class="card">
<div class="title">
<h1>{{title}}</h1>
</div>
<div class="list">
<hr>
<ul>
{{#scopes}}
<li>
<div style="display:inline-block">
{{#if logo}}
<div class="image">
<img width="50px" height="50px" src="{{logo}}">
</div>
{{/if}}
<div class="text">
<h3 class="scope_title">{{name}}</h3>
<p class="scope_description">{{description}}</p>
</div>
</div>
<hr>
</li>
{{/scopes}}
</ul>
</div>
<div>
{{information}}
</div>
<div>
<div style="text-align:right;">
<button class="mdc-button mdc-button--raised grey-button" id="cancel">Cancel</button>
<button class="mdc-button mdc-button--raised blue-button" id="allow">Allow</button>
</div>
</div>
</div>
<form method="post" action="/api/oauth/auth?" id="hidden_form" style="display: none;"></form>
</body>
</html>

View File

@ -0,0 +1,15 @@
document.getElementById("hidden_form").action += window.location.href.split("?")[1];
function submit() {
document.getElementById("hidden_form").submit();
}
document.getElementById("cancel").onclick = () => {
let u = new URL(window.location);
let uri = u.searchParams.get("redirect_uri");
window.location.href = uri + "?error=access_denied&state=" + u.searchParams.get("state");
}
document.getElementById("allow").onclick = () => {
submit()
}

View File

@ -0,0 +1,72 @@
@import "@material/button/mdc-button";
.blue-button {
background: #4a89dc !important;
}
.grey-button {
background: #797979 !important;
}
hr {
// display: block;
// height: 1px;
border: 0;
border-top: 1px solid #b8b8b8;
// margin: 1em 0;
// padding: 0;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
.title {
text-align:center;
}
h1, h3 { font-weight: 300; }
h1 { color: #636363; }
ul {
list-style: none;
padding-left: 0;
}
.image {
display: block;
height: 50px;
width: 50px;
float: left;
}
.text {
display: block;
width: calc(100% - 60px);
height: 50px;
float: right;
padding-left: 10px;
}
.scope_title {
margin-top: 0;
margin-bottom: 0;
padding-left: 5px;
}
.scope_description {
margin-top: 0;
padding-left: 15px;
font-size: 13px;
color: #202020;
}
.card {
max-width: 480px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0,0,0,0.14902) 0px 1px 1px 0px,rgba(0,0,0,0.09804) 0px 1px 2px 0px;
}

47
views/src/login/login.hbs Normal file
View File

@ -0,0 +1,47 @@
<html>
<head>
<title>{{i18n "Login"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<hgroup>
<h1>{{i18n "Login"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
<div class="loader_box" id="loader">
<div class="loader"></div>
</div>
<div id="container">
<div class="floating group" id="usernamegroup">
<input type="text" id="username" autofocus>
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Username or Email"}}</label>
<div class="error invisible" id="uerrorfield"></div>
</div>
<div class="floating group invisible" id="passwordgroup">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Password"}}</label>
<div class="error invisible" id="perrorfield"></div>
</div>
<button type="button" id="nextbutton" class="mdc-button mdc-button--raised">{{i18n "Next"}}
</button>
<button type="button" id="loginbutton" class="mdc-button mdc-button--raised invisible">{{i18n "Login"}}
</button>
</div>
</form>
<footer>
<!-- <a href="http://www.polymer-project.org/" target="_blank">
<img src="https://www.polymer-project.org/images/logos/p-logo.svg">
</a> -->
<p>Powered by {{appname}}</p>
</footer>
</body>
</html>

140
views/src/login/login.js Normal file
View File

@ -0,0 +1,140 @@
import sha from "sha512";
import {
setCookie,
getCookie
} from "cookie"
import "inputs"
const loader = document.getElementById("loader")
const container = document.getElementById("container")
const usernameinput = document.getElementById("username")
const usernamegroup = document.getElementById("usernamegroup")
const uerrorfield = document.getElementById("uerrorfield")
const passwordinput = document.getElementById("password")
const passwordgroup = document.getElementById("passwordgroup")
const perrorfield = document.getElementById("perrorfield")
const nextbutton = document.getElementById("nextbutton")
const loginbutton = document.getElementById("loginbutton")
let username;
let salt;
usernameinput.focus()
const loading = () => {
container.style.filter = "blur(2px)";
loader.style.display = "";
}
const loading_fin = () => {
container.style.filter = ""
loader.style.display = "none";
}
loading_fin();
usernameinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) nextbutton.click();
clearError(uerrorfield);
}
nextbutton.onclick = async () => {
loading();
username = usernameinput.value;
try {
let res = await fetch("/api/user/login?type=username&username=" + username, {
method: "POST"
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
salt = res.salt;
usernamegroup.classList.add("invisible")
nextbutton.classList.add("invisible")
passwordgroup.classList.remove("invisible")
loginbutton.classList.remove("invisible")
passwordinput.focus()
} catch (e) {
showError(uerrorfield, e.message)
}
loading_fin()
}
passwordinput.onkeydown = (e) => {
var keycode = e.keyCode ? e.keyCode : e.which;
if (keycode === 13) loginbutton.click();
clearError(perrorfield);
}
loginbutton.onclick = async () => {
loading();
let pw = sha(salt + passwordinput.value);
try {
let {
login,
special
} = await fetch("/api/user/login?type=password", {
method: "POST",
body: JSON.stringify({
username: usernameinput.value,
password: pw
}),
headers: {
'content-type': 'application/json'
},
}).then(e => {
if (e.status !== 200) throw new Error(e.statusText)
return e.json()
}).then(data => {
if (data.error) {
return Promise.reject(new Error(data.error))
}
return data;
})
setCookie("login", login.token, login.expires)
setCookie("special", special.token, special.expires)
let d = new Date()
d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
setCookie("username", username, d.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;
} catch (e) {
passwordinput.value = "";
showError(perrorfield, e.message);
}
loading_fin();
}
function clearError(field) {
field.innerText = "";
field.classList.add("invisible")
}
function showError(field, error) {
field.innerText = error;
field.classList.remove("invisible")
}
username = getCookie("username")
if (username) {
usernameinput.value = username;
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
usernameinput.dispatchEvent(evt);
}

129
views/src/login/login.scss Normal file
View File

@ -0,0 +1,129 @@
@import "@material/button/mdc-button";
@import "inputs";
@import "style";
#loginbutton,
#nextbutton {
width: 100%;
background: $primary; // text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
}
* {
box-sizing: border-box;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
hgroup {
text-align: center;
margin-top: 4em;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
h3 {
color: $primary;
}
form {
max-width: 380px;
margin: 4em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
position: relative;
}
.loader_box {
width: 64px;
height: 64px;
margin: auto;
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
}
.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 #000000;
border-color: #000000 transparent #000000 transparent;
animation: loader 1.2s linear infinite;
}
@keyframes loader {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
footer {
text-align: center;
}
footer p {
color: #888;
font-size: 13px;
letter-spacing: .4px;
}
footer a {
color: $primary;
text-decoration: none;
transition: all .2s ease;
}
footer a:hover {
color: #666;
text-decoration: underline;
}
footer img {
width: 80px;
transition: all .2s ease;
}
footer img:hover {
opacity: .83;
}
footer img:focus,
footer a:focus {
outline: none;
}
.invisible {
display: none;
}
.errorColor {
background: $error !important;
}
.error {
color: $error;
margin-top: 5px;
font-size: 13px;
}

7
views/src/main/main.hbs Normal file
View File

@ -0,0 +1,7 @@
<html>
<head></head>
<body></body>
</html>

1
views/src/main/main.js Normal file
View File

@ -0,0 +1 @@
console.log("Hello World")

0
views/src/main/main.scss Normal file
View File

View File

@ -0,0 +1,115 @@
<html>
<head>
<title>{{i18n "Register"}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
<hgroup>
<h1>{{i18n "Register"}}</h1>
</hgroup>
<form action="JavaScript:void(0)">
<div class="error invisible" id="error" style="font-size: 18px; margin-bottom: 16px"></div>
<div class="floating group">
<input type="text" id="regcode">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Registration code"}}</label>
<div class="error invisible" id="err_regcode"></div>
</div>
<div class="floating group">
<input type="text" id="username">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Username"}}</label>
<div class="error invisible" id="err_username"></div>
</div>
<div class="floating group">
<input type="text" id="name">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Name"}}</label>
<div class="error invisible" id="err_name"></div>
</div>
<div class="error invisible" id="err_gender"></div>
<div class="mdc-form-field group">
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-male" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-male" style="position: relative;">{{i18n "Male"}}</label>
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-female" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-female" style="position: relative;">{{i18n "Female"}}</label>
<div class="mdc-radio">
<input class="mdc-radio__native-control" type="radio" id="radio-other" name="radios" checked>
<div class="mdc-radio__background">
<div class="mdc-radio__outer-circle"></div>
<div class="mdc-radio__inner-circle"></div>
</div>
</div>
<label for="radio-other" style="position: relative;">{{i18n "Other"}}</label>
</div>
<div class="floating group">
<input type="text" id="mail">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Mail"}}</label>
<div class="error invisible" id="err_mail"></div>
</div>
<div class="floating group">
<input type="password" id="password">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Password"}}</label>
<div class="error invisible" id="err_password"></div>
</div>
<div class="floating group">
<input type="password" id="passwordrep">
<span class="highlight"></span>
<span class="bar"></span>
<label>{{i18n "Repeat Password"}}</label>
<div class="error invisible" id="err_passwordrep"></div>
</div>
<button type="button" id="registerbutton" class="mdc-button mdc-button--raised">{{i18n "Register"}}
</button>
</form>
<footer>
<p>Powered by {{appname}}</p>
</footer>
<script type="json" id="error_codes">
{
"noregcode": "{{i18n "Registration code required"}}",
"nousername": "{{i18n "Username required"}}",
"noname": "{{i18n "Name required"}}",
"nomail": "{{i18n "Mail required"}}",
"nogender": "{{i18n "You need to select one of the options"}}",
"nopassword": "{{i18n "Password is required"}}",
"nomatch": "{{i18n "The passwords do not match"}}"
}
</script>
</body>
</html>

View File

@ -0,0 +1,149 @@
import "inputs";
import sha from "sha512";
import fireEvent from "event"
(() => {
const translations = JSON.parse(document.getElementById("error_codes").innerText)
const regcode = document.getElementById("regcode")
regcode.value = new URL(window.location.href).searchParams.get("regcode")
fireEvent(regcode, "change");
function showError(element, message) {
if (typeof element === "string")
element = document.getElementById(element)
if (!element) console.error("Element not found,", element)
element.innerText = message;
if (!message) {
if (!element.classList.contains("invisible"))
element.classList.add("invisible")
} else {
element.classList.remove("invisible");
}
}
function makeid(length) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < length; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
const username = document.getElementById("username")
const name = document.getElementById("name")
const mail = document.getElementById("mail")
const password = document.getElementById("password")
const passwordrep = document.getElementById("passwordrep")
const radio_male = document.getElementById("radio-male")
const radio_female = document.getElementById("radio-female")
const radio_other = document.getElementById("radio-other")
const registerButton = document.getElementById("registerbutton")
registerButton.onclick = () => {
console.log("Register")
showError("error");
let error = false;
if (!regcode.value) {
showError("err_regcode", translations["noregcode"])
error = true;
} else {
showError("err_regcode")
}
if (!username.value) {
showError("err_username", translations["nousername"])
error = true;
} else {
showError("err_username")
}
if (!name.value) {
showError("err_name", translations["noname"])
error = true;
} else {
showError("err_name")
}
if (!mail.value) {
showError("err_mail", translations["nomail"])
error = true;
} else {
showError("err_mail")
}
if (!password.value) {
showError("err_password", translations["nopassword"])
error = true;
} else {
showError("err_password")
}
if (password.value !== passwordrep.value) {
showError("err_passwordrep", translations["nomatch"])
error = true;
} else {
showError("err_passwordrep")
}
if (error) return;
let gender;
if (radio_male.checked) {
gender = "male"
} else if (radio_female.checked) {
gender = "female"
} else {
gender = "other"
}
let salt = makeid(10)
//username, password, salt, mail, gender, name, birthday, regcode
let body = {
username: username.value,
gender: gender,
mail: mail.value,
name: name.value,
regcode: regcode.value,
salt: salt,
password: sha(salt + password.value)
}
fetch("/api/user/register", {
method: "POST",
body: JSON.stringify(body),
headers: {
'content-type': 'application/json'
},
}).then(async e => {
if (e.status !== 200) return Promise.reject(new Error(await e.text() || e.statusText));
return e.json()
}).then(data => {
if (data.error) {
if (!Array.isArray(data.error)) return Promise.reject(new Error(data.error));
let ce = [];
data.error.forEach(e => {
let ef = document.getElementById("err_" + e.field);
if (!ef) ce.push(e);
else {
showError(ef, e.message);
}
})
if (ce.length > 0) {
showError("error", ce.join("<br>"));
}
} else {
window.location.href = "/login";
}
}).catch(e => {
showError("error", e.message);
})
}
})()

View File

@ -0,0 +1,95 @@
@import "@material/button/mdc-button";
@import "@material/form-field/mdc-form-field";
@import "@material/radio/mdc-radio";
@import "inputs";
* {
box-sizing: border-box;
}
body {
font-family: Helvetica;
background: #eee;
-webkit-font-smoothing: antialiased;
}
hgroup {
text-align: center;
margin-top: 1em;
}
h1,
h3 {
font-weight: 300;
}
h1 {
color: #636363;
}
h3 {
color: $primary;
}
form {
max-width: 380px;
margin: 1em auto;
padding: 3em 2em 2em 2em;
background: #fafafa;
border: 1px solid #ebebeb;
box-shadow: rgba(0, 0, 0, 0.14902) 0px 1px 1px 0px, rgba(0, 0, 0, 0.09804) 0px 1px 2px 0px;
}
#registerbutton {
width: 100%;
background: $primary;
text-shadow: 1px 1px 0 rgba(39, 110, 204, .5);
}
footer {
text-align: center;
}
footer p {
color: #888;
font-size: 13px;
letter-spacing: .4px;
}
footer a {
color: $primary;
text-decoration: none;
transition: all .2s ease;
}
footer a:hover {
color: #666;
text-decoration: underline;
}
footer img {
width: 80px;
transition: all .2s ease;
}
footer img:hover {
opacity: .83;
}
footer img:focus,
footer a:focus {
outline: none;
}
.invisible {
display: none;
}
.errorColor {
background: $error !important;
}
.error {
color: $error;
margin-top: 5px;
font-size: 13px;
}

14
views/src/user/user.hbs Normal file
View File

@ -0,0 +1,14 @@
<html>
<head>
<title>{{title}}</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
</head>
<body>
</body>
</html>

1
views/src/user/user.js Normal file
View File

@ -0,0 +1 @@
console.log("Hello World")

0
views/src/user/user.scss Normal file
View File