First version of OpenAuth remake
This commit is contained in:
160
views/build.js
Normal file
160
views/build.js
Normal file
@ -0,0 +1,160 @@
|
||||
const {
|
||||
lstatSync,
|
||||
readdirSync,
|
||||
mkdirSync,
|
||||
copyFileSync,
|
||||
writeFileSync,
|
||||
readFileSync
|
||||
} = require('fs')
|
||||
const {
|
||||
join,
|
||||
basename
|
||||
} = require('path')
|
||||
const includepaths = require("rollup-plugin-includepaths")
|
||||
|
||||
const isDirectory = source => lstatSync(source).isDirectory()
|
||||
const getDirectories = source =>
|
||||
readdirSync(source).map(name => join(source, name)).filter(isDirectory)
|
||||
|
||||
function ensureDir(folder) {
|
||||
try {
|
||||
if (!isDirectory(folder)) mkdirSync(folder)
|
||||
} catch (e) {
|
||||
mkdirSync(folder)
|
||||
}
|
||||
}
|
||||
|
||||
ensureDir("./out")
|
||||
|
||||
const sass = require('sass');
|
||||
|
||||
function findHead(elm) {
|
||||
if (elm.tagName === "head") return elm;
|
||||
for (let i = 0; i < elm.childNodes.length; i++) {
|
||||
let res = findHead(elm.childNodes[i])
|
||||
if (res) return res;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const rollup = require("rollup")
|
||||
const minify = require("html-minifier").minify
|
||||
const gzipSize = require('gzip-size');
|
||||
|
||||
async function buildPage(folder, name) {
|
||||
const pagename = basename(folder);
|
||||
const outpath = "./out/" + pagename;
|
||||
|
||||
ensureDir(outpath)
|
||||
|
||||
let bundle = await rollup.rollup({
|
||||
input: `${folder}/${pagename}.js`,
|
||||
plugins: [includepaths({
|
||||
paths: ["shared"]
|
||||
})],
|
||||
})
|
||||
|
||||
let {
|
||||
code,
|
||||
map
|
||||
} = await bundle.generate({
|
||||
format: "iife",
|
||||
|
||||
})
|
||||
|
||||
let sass_res = sass.renderSync({
|
||||
file: folder + `/${pagename}.scss`,
|
||||
includePaths: ["./node_modules", folder, "./shared"],
|
||||
outputStyle: "compressed"
|
||||
})
|
||||
|
||||
let css = "<style>\n" + sass_res.css.toString("utf8") + "\n</style>\n";
|
||||
let script = "<script>\n" + code + "\n</script>\n";
|
||||
let html = readFileSync(`${folder}/${pagename}.hbs`).toString("utf8");
|
||||
|
||||
let idx = html.indexOf("</head>")
|
||||
if (idx < 0) throw new Error("No head element found")
|
||||
let idx2 = html.indexOf("</body>")
|
||||
if (idx2 < 0) throw new Error("No body element found")
|
||||
|
||||
if (idx < idx2) {
|
||||
let part1 = html.slice(0, idx)
|
||||
let part2 = html.slice(idx, idx2);
|
||||
let part3 = html.slice(idx2, html.length);
|
||||
html = part1 + css + part2 + script + part3;
|
||||
} else {
|
||||
let part1 = html.slice(0, idx2)
|
||||
let part2 = html.slice(idx2, idx);
|
||||
let part3 = html.slice(idx, html.length);
|
||||
html = part1 + script + part2 + css + part3;
|
||||
}
|
||||
|
||||
let result = minify(html, {
|
||||
removeAttributeQuotes: true,
|
||||
collapseWhitespace: true,
|
||||
html5: true,
|
||||
keepClosingSlash: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeComments: true,
|
||||
useShortDoctype: true
|
||||
})
|
||||
|
||||
let gzips = await gzipSize(result)
|
||||
writeFileSync(`${outpath}/${pagename}.html`, result)
|
||||
|
||||
let stats = {
|
||||
sass: sass_res.stats,
|
||||
js: {
|
||||
chars: code.length
|
||||
},
|
||||
css: {
|
||||
chars: css.length
|
||||
},
|
||||
bundle_size: result.length,
|
||||
gzip_size: gzips
|
||||
}
|
||||
|
||||
writeFileSync(outpath + `/stats.json`, JSON.stringify(stats, null, " "))
|
||||
}
|
||||
|
||||
async function run() {
|
||||
const pages = getDirectories("./src");
|
||||
const ProgressBar = require('progress');
|
||||
// const bar = new ProgressBar('[:bar] :current/:total :percent :elapseds :etas', {
|
||||
// // schema: '[:bar] :current/:total :percent :elapseds :etas',
|
||||
// total: pages.length
|
||||
// });
|
||||
await Promise.all(pages.map(async e => {
|
||||
try {
|
||||
await buildPage(e)
|
||||
} catch (er) {
|
||||
console.error("Failed compiling", basename(e))
|
||||
console.log(er.message)
|
||||
}
|
||||
// bar.tick()
|
||||
}))
|
||||
console.log("Finished compiling!")
|
||||
|
||||
}
|
||||
|
||||
|
||||
if (process.argv.join(" ").toLowerCase().indexOf("watch") < 0) {
|
||||
run()
|
||||
} else {
|
||||
const nodemon = require('nodemon');
|
||||
|
||||
nodemon({
|
||||
script: "dummy.js",
|
||||
ext: 'js hbs scss',
|
||||
ignore: ["out/"]
|
||||
});
|
||||
|
||||
nodemon.on('start', function () {
|
||||
run()
|
||||
}).on('quit', function () {
|
||||
process.exit();
|
||||
}).on('restart', function (files) {
|
||||
// console.log('App restarted due to: ', files);
|
||||
});
|
||||
}
|
0
views/dummy.js
Normal file
0
views/dummy.js
Normal file
3240
views/package-lock.json
generated
Normal file
3240
views/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
views/package.json
Normal file
27
views/package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "open_auth_service_views",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "Fabian Stamm <dev@fabianstamm.de>",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"watch": "node build.js watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@material/button": "^0.41.0",
|
||||
"@material/form-field": "^0.41.0",
|
||||
"@material/radio": "^0.41.0",
|
||||
"ascii-progress": "^1.0.5",
|
||||
"html-minifier": "^3.5.21",
|
||||
"jsdom": "^13.0.0",
|
||||
"nodemon": "^1.18.6",
|
||||
"progress": "^2.0.1",
|
||||
"rollup": "^0.67.0",
|
||||
"rollup-plugin-includepaths": "^0.2.3",
|
||||
"sass": "^1.14.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gzip-size": "^5.0.0"
|
||||
}
|
||||
}
|
20
views/shared/cookie.js
Normal file
20
views/shared/cookie.js
Normal file
@ -0,0 +1,20 @@
|
||||
export function setCookie(cname, cvalue, exdate) {
|
||||
var expires = exdate ? `expires=${exdate};` : "";
|
||||
document.cookie = `${cname}=${cvalue};${expires}path=/`
|
||||
}
|
||||
|
||||
export function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var decodedCookie = decodeURIComponent(document.cookie);
|
||||
var ca = decodedCookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0) == ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) == 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
13
views/shared/event.js
Normal file
13
views/shared/event.js
Normal file
@ -0,0 +1,13 @@
|
||||
export default function fireEvent(element, event) {
|
||||
if (document.createEventObject) {
|
||||
// dispatch for IE
|
||||
var evt = document.createEventObject();
|
||||
return element.fireEvent('on' + event, evt)
|
||||
}
|
||||
else {
|
||||
// dispatch for firefox + others
|
||||
var evt = document.createEvent("HTMLEvents");
|
||||
evt.initEvent(event, true, true); // event type,bubbling,cancelable
|
||||
return !element.dispatchEvent(evt);
|
||||
}
|
||||
}
|
14
views/shared/formdata.js
Normal file
14
views/shared/formdata.js
Normal file
@ -0,0 +1,14 @@
|
||||
export default function getFormData(element) {
|
||||
let data = {};
|
||||
if (element.name !== undefined && element.name !== null && element.name !== "") {
|
||||
if (typeof element.name === "string") {
|
||||
if (element.type === "checkbox") data[element.name] = element.checked;
|
||||
else data[element.name] = element.value;
|
||||
}
|
||||
}
|
||||
element.childNodes.forEach(child => {
|
||||
let res = getFormData(child);
|
||||
data = Object.assign(data, res);
|
||||
})
|
||||
return data;
|
||||
}
|
13
views/shared/inputs.js
Normal file
13
views/shared/inputs.js
Normal file
@ -0,0 +1,13 @@
|
||||
document.querySelectorAll(".floating>input").forEach(e => {
|
||||
function checkState() {
|
||||
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()
|
||||
})
|
113
views/shared/inputs.scss
Normal file
113
views/shared/inputs.scss
Normal file
@ -0,0 +1,113 @@
|
||||
@import "style";
|
||||
.group {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
min-height: 45px;
|
||||
}
|
||||
|
||||
.floating>input {
|
||||
font-size: 18px;
|
||||
padding: 10px 10px 10px 5px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
background: #fafafa;
|
||||
color: #636363;
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #757575;
|
||||
}
|
||||
|
||||
.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: $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: $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: $primary;
|
||||
}
|
||||
to {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
2
views/shared/mat_bs.scss
Normal file
2
views/shared/mat_bs.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons");
|
||||
@import url("https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css");
|
16
views/shared/request.js
Normal file
16
views/shared/request.js
Normal file
@ -0,0 +1,16 @@
|
||||
export default function request(endpoint, method, data) {
|
||||
var headers = new Headers();
|
||||
headers.set('Content-Type', 'application/json');
|
||||
return fetch(endpoint, {
|
||||
method: method,
|
||||
body: JSON.stringify(data),
|
||||
headers: headers,
|
||||
credentials: "include"
|
||||
}).then(async e => {
|
||||
if (e.status !== 200) throw new Error(await e.text() || e.statusText);
|
||||
return e.json()
|
||||
}).then(e => {
|
||||
if (e.error) return Promise.reject(new Error(typeof e.error === "string" ? e.error : JSON.stringify(e.error)));
|
||||
return e;
|
||||
})
|
||||
}
|
1
views/shared/sha512.js
Normal file
1
views/shared/sha512.js
Normal file
File diff suppressed because one or more lines are too long
7
views/shared/style.scss
Normal file
7
views/shared/style.scss
Normal file
@ -0,0 +1,7 @@
|
||||
// $primary: #4a89dc;
|
||||
$primary: #1E88E5;
|
||||
$error: #ff2f00;
|
||||
.btn-primary {
|
||||
color: white !important;
|
||||
background-color: $primary !important;
|
||||
}
|
244
views/src/admin/admin.hbs
Normal file
244
views/src/admin/admin.hbs
Normal 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
158
views/src/admin/admin.js
Normal 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
103
views/src/admin/admin.scss
Normal 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;
|
||||
}
|
51
views/src/authorize/authorize.hbs
Normal file
51
views/src/authorize/authorize.hbs
Normal 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>
|
15
views/src/authorize/authorize.js
Normal file
15
views/src/authorize/authorize.js
Normal 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()
|
||||
}
|
72
views/src/authorize/authorize.scss
Normal file
72
views/src/authorize/authorize.scss
Normal 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
47
views/src/login/login.hbs
Normal 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
140
views/src/login/login.js
Normal 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
129
views/src/login/login.scss
Normal 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
7
views/src/main/main.hbs
Normal file
@ -0,0 +1,7 @@
|
||||
<html>
|
||||
|
||||
<head></head>
|
||||
|
||||
<body></body>
|
||||
|
||||
</html>
|
1
views/src/main/main.js
Normal file
1
views/src/main/main.js
Normal file
@ -0,0 +1 @@
|
||||
console.log("Hello World")
|
0
views/src/main/main.scss
Normal file
0
views/src/main/main.scss
Normal file
115
views/src/register/register.hbs
Normal file
115
views/src/register/register.hbs
Normal 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>
|
149
views/src/register/register.js
Normal file
149
views/src/register/register.js
Normal 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);
|
||||
})
|
||||
}
|
||||
})()
|
95
views/src/register/register.scss
Normal file
95
views/src/register/register.scss
Normal 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
14
views/src/user/user.hbs
Normal 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
1
views/src/user/user.js
Normal file
@ -0,0 +1 @@
|
||||
console.log("Hello World")
|
0
views/src/user/user.scss
Normal file
0
views/src/user/user.scss
Normal file
2233
views/yarn.lock
Normal file
2233
views/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user