Merge pull request #2 from PretendoNetwork/dev

Dev
This commit is contained in:
RedDucks 2018-11-23 20:31:51 -05:00 committed by GitHub
commit 705a52a641
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1660 additions and 466 deletions

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
# web javascript causes a lot of eslint warnings
assets/js/*.js

View File

@ -3,7 +3,10 @@
"node": true,
"commonjs": true,
"es6": true
},
},
"globals": {
"document": true
},
"parserOptions": {
"ecmaVersion": 2017
},

View File

@ -1,30 +0,0 @@
@font-face {
font-family: 'icomoon';
src: url('/assets/fonts/icomoon.eot?ybqupz');
src: url('/assets/fonts/icomoon.eot?ybqupz#iefix') format('embedded-opentype'),
url('/assets/fonts/icomoon.ttf?ybqupz') format('truetype'),
url('/assets/fonts/icomoon.woff?ybqupz') format('woff'),
url('/assets/fonts/icomoon.svg?ybqupz#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="pretendo-icon-"], [class*=" pretendo-icon-"] {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'icomoon' !important;
speak: none;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
font-size: 1em;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.pretendo-icon-pretendo-logo:before {
content: "\e900";
}

View File

@ -0,0 +1,45 @@
body.flex.center {
justify-content: center;
align-items: center;
}
body footer {
padding: 0;
background-color: transparent;
}
body footer .content {
border-radius: 0;
background-color: transparent;
padding: 1em 0;
box-shadow: none;
width: 30rem;
max-width: 100vw;
}
/* login/register card */
.card {
background-color: white;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
width: 35rem;
max-width: 100%;
}
.card .content {
padding: 0 3em 4em 3em;
}
.card .head {
background-color: #673ab7;
color: white;
font-size: 1em;
font-weight: normal;
padding: 1.5em 2em;
margin: 0;
}
.card .content .btnMargin {
display: block;
width: 100%;
margin: 3em 0 0 0;
}
.card .content .lowText {
text-align: right;
color: #707070;
}

View File

@ -0,0 +1,379 @@
/*******************************************************/
/* copyrighted by pretendo. */
/* website design by jipfr, mrjvs and benthehuman */
/*******************************************************/
/* fonts */
@import url('https://fonts.googleapis.com/css?family=Roboto:400,700');
/* setup */
body {
margin: 0;
font-family: 'Roboto', Arial, Helvetica, sans-serif;
font-weight: 400;
background: #efefef;
overflow-x: hidden;
}
body.flex {
display: flex;
flex-direction: column;
min-height: 100vh;
}
body.flex .contentWrapper {
flex: 1;
}
/* common header */
nav.navWrapper {
background: #673db6;
color: white;
display: flex;
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 10;
justify-content: center;
}
nav.navWrapper .navItem {
flex: 0;
padding: 15px 20px;
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
font-size: 1em;
cursor: pointer;
align-self: stretch;
}
nav.navWrapper .navItem:hover, nav.navWrapper .navItem.active {
color: white;
text-decoration: none;
background: rgba(0, 0, 0, 0.07);
}
nav.navWrapper .navItem.brand {
font-weight: 600;
background: #5231a7;
padding: 15px 40px;
color: white;
}
nav.navWrapper .navBtn, nav.navWrapper .navBtnAlt {
flex: 0;
align-self: center;
padding: 5px 30px;
color: white;
text-decoration: none;
font-size: 1em;
cursor: pointer;
background-color: #512da8;
border-radius: 5px;
margin: 0 10px;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
}
nav.navWrapper .navBtnAlt {
color: #3d1c8a;
background-color: white;
}
nav.navWrapper .navBtn:hover {
background-color: #52339b;
}
nav.navWrapper .navBtnAlt:hover {
background-color: rgba(255, 255, 255, 0.745);
color: #54359b;
}
nav.navWrapper .navItem:nth-last-child(2) {
margin-right: 20px;
}
nav.navWrapper .navProfile {
align-self: stretch;
display: flex;
align-items: center;
padding: 0 20px;
}
nav.navWrapper .navProfile img {
border-radius: 50%;
width: 2em; height: 2em;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.5);
}
nav.navWrapper .navProfile p {
padding: 0 0 0 10px;
}
nav.navWrapper .navSpread {
flex: 1;
}
/* mobile nav */
nav.navWrapper .navItem.hamburger {
display: none;
position: absolute;
right: 0;
top: 0;
}
@media screen and (max-width: 720px) {
nav.navWrapper {
flex-direction: column;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
}
nav.navWrapper .navItem:not(.brand) {
display: none;
}
nav.navWrapper.open .navItem, nav.navWrapper .navItem.hamburger {
display: block;
}
nav.navWrapper.open .navItem.navBtn, nav.navWrapper.open .navItem.navBtnAlt {
padding: .5em 0;
width: 12em;
text-align: center;
margin: .5em 0;
}
nav.navWrapper .navItem:nth-last-child(2) {
margin-right: 0;
}
nav.navWrapper.open .navItem.navProfile {
align-self: center;
display: flex;
}
}
/* content wrapper */
.contentWrapper {
position: relative;
width: 100%;
}
nav.navWrapper ~ .contentWrapper:nth-child(2) {
margin-top: 3rem;
}
.contentWrapper .bgExtrude {
position: absolute;
top: 0;
left: 0;
width: 100%;
background: #673db6;
height: 10rem;
z-index: 2;
}
.contentWrapper.bgLines::before, .contentWrapper.bgLines::after,
.contentWrapper.bgLinesSmall::before, .contentWrapper.bgLinesSmall::after {
content: "";
position: absolute;
z-index: 3;
width: 0;
height: 0;
border-style: solid;
border-width: 100vh 0 0 200vw;
border-color: transparent transparent transparent #673db6;
left: 0;
bottom: 0;
}
.contentWrapper.bgLines::after, .contentWrapper.bgLinesSmall::after {
border-width: 0 0 100vh 200vw;
border-color: transparent transparent #9575CD transparent;
right: 0;
left: auto;
z-index: 2;
}
.contentWrapper.bgLinesSmall::after {
border-width: 0 0 18vh 150vw;
}
.contentWrapper.bgLinesSmall::before {
border-width: 20vh 0 0 150vw;
}
.contentWrapper > * {
z-index: 3;
position: relative;
}
/* footer */
footer {
background: #673db6;
padding: 2em 1em;
}
footer .content {
background: white;
border-radius: 5px;
padding: 1em;
text-align: center;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
color: #707070;
}
footer .content span {
color: black;
}
/* big card */
section.bigCardWrapper {
padding: 20px;
}
section.bigCardWrapper .bigCard {
max-width: 80vw;
margin: auto;
background-color: white;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
padding: 5vw 12vw;
box-sizing: border-box;
color: #707070;
}
section.bigCardWrapper .bigCard.lessPadding {
padding: 4vw 5vw;
}
section.bigCardWrapper .bigCard.bigCardPartWrapper {
display: flex;
}
/* text styles */
.txt-highlight {
color: #673db6;
margin-bottom: 0;
font-size: 1rem;
font-weight: bold;
}
.txt-title {
color: #191919;
margin-top: 10px;
margin-bottom: 15px;
font-size: 2.5em;
}
.txt-title.small {
font-size: 2em;
}
.txt-smallHeading {
font-size: 1.2em;
font-weight: bold;
color: #383838;
}
.txt-center {
text-align: center;
}
/* components - buttons */
a.btn, a.btn-alt,
a.btn:focus, a.btn-alt:focus,
a.btn:hover, a.btn-alt:hover {
text-decoration: none;
}
button, .btn, .btn-alt {
margin: 0;
padding: 0;
border: 0;
border-radius: 0;
font-family: 'Roboto';
font-size: 1em;
cursor: pointer;
}
.btn, button {
background: #673db6;
color: white;
padding: 12px 80px;
border-radius: 10px;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.17);
}
.btn-alt {
background: white;
padding: 10px 45px;
color: #673db6;
border: 1px solid #673db6;
box-shadow: 0 3px 12px rgba(0, 0, 0, 0.07);
border-radius: 5px;
}
.btn:hover, button:hover, .btn-alt:hover {
background: #553099;
color: white;
}
/* components - profiles */
/* profiles */
.cardProfiles {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.profile {
min-width: 250px;
width: 50%;
max-width: 550px;
margin-bottom: 10px;
display: flex;
}
.profileIcon {
margin-right: 10px;
width: 80px; height: 80px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.5);
}
.profileText {
width: calc(100% - 130px);
min-width: 200px;
display: flex;
flex-direction: column;
justify-content: center;
}
.profileText h2 {
margin: 0;
}
.profileText p {
margin-top: 5px;
}
/* components - inputs */
input::placeholder, textarea::placeholder {
color: #bebebe;
opacity: 1;
}
input[type="text"], input[type="password"], textarea {
display: inline-block;
width: 100%;
padding: 1em 2em;
border-style: solid;
border-color: rgb(165, 165, 165);
border-width: 1px;
border-radius: 8px;
margin: 1em 0;
font-family: 'Roboto', Arial, Helvetica, sans-serif;
font-weight: 400;
font-size: 1rem;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
transition: border-color .1s;
box-sizing: border-box;
resize: none;
}
label {
font-size: 1em;
margin: 2em 0 .2em .2em;
display: block;
}
input[type="text"].hasLabel, input[type="password"].hasLabel {
margin: 0;
}
textarea {
height: 12rem;
}
input[type="text"]:focus, input[type="password"]:focus, textarea:focus {
border-color: rgb(103, 61, 182);
box-shadow: 0 3px 10px 0px rgba(103, 61, 182, 0.07);
}
/* helper classes */
.cont-flex {
display: flex;
}
.flex-item {
flex: 1;
}
@media screen and (max-width: 500px) {
section.bigCardWrapper {
padding: 0;
padding-bottom: 3em;
}
section.bigCardWrapper .bigCard {
max-width: 100vw;
padding: 50px;
}
}

View File

@ -0,0 +1,15 @@
.input-container {
display: flex;
flex-direction: row;
}
.input-container > * {
flex: 1;
}
.input-container > *:first-child {
margin-right: 3em;
}
@media screen and (max-width: 1000px) {
.input-container {
flex-direction: column;
}
}

View File

@ -0,0 +1,93 @@
/* home header */
header {
text-align: center;
color: white;
background: #673db6;
padding: 5rem 1rem;
}
header hr {
width: 175px;
height: 2px;
background: white;
display: inline-block;
border: 0;
margin: 0 auto;
display: block;
}
header h1 {
font-size: 3.5em;
font-weight: 400;
letter-spacing: 0.25em;
margin: 0;
margin-bottom: 10px;
text-transform: uppercase;
}
header h3 {
font-weight: normal;
max-width: 80%;
display: inline-block;
font-size: 1.1rem;
}
/* news cards */
.smallCard {
padding: 20px;
border: 1px solid #9474cc; border-radius: 10px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.07);
position: relative;
margin-left: 3em;
height: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.smallCard h3 {
font-size: 1.5em;
margin: 0;
}
.smallCard p {
flex: 1;
}
/* specific */
.smallCardWidth {
width: 100%;
max-width: 300px;
}
@media screen and (max-width: 1050px) {
.bigCard.cont-flex {
flex-direction: column;
}
.bigCard > .flex-item {
margin-bottom: 3em;
}
.smallCardWidth .smallCard {
margin-left: 0;
margin: 1em;
}
.bigCard.cont-flex > .cont-flex {
align-items: center;
justify-content: center;
}
}
@media screen and (max-width: 888px) {
.profile {
width: 100%;
max-width: 100%;
min-width: 100%;
}
.smallCard {
width: 100%;
max-width: 250px;
height: auto;
}
.bigCard.cont-flex > .cont-flex {
flex-direction: column;
}
header h1 {
font-size: 2.5em;
}
}

View File

@ -0,0 +1,26 @@
/* news cards */
.smallCard {
padding: 2em;
border: 1px solid rgb(165, 165, 165);
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.07);
position: relative;
margin: .5em;
width: calc(50% - 1em );
box-sizing: border-box;
display: inline-block;
}
.smallCard h3 {
font-size: 1.5em;
margin: 0;
}
.smallCard a {
display: inline-block;
}
@media screen and (max-width: 720px) {
.smallCard {
width: 100%;
display: block;
}
}

View File

@ -0,0 +1,72 @@
body .contentWrapper.userPage {
display: flex;
justify-content: center;
height: 100%;
margin: 10rem 0 7rem 0 !important; /*im sorry, I had to*/
}
body footer {
padding: 0;
box-shadow: 0 -3px 10px 0px rgba(0, 0, 0, 0.07);
}
body footer .content {
border-radius: 0;
}
/* content card */
.contentCard {
background-color: white;
box-shadow: 0 3px 10px 0px rgba(0, 0, 0, 0.07);
box-sizing: border-box;
color: #707070;
width: 40rem;
max-width: 100vw;
padding: 3rem 4rem;
}
/* aside navigation */
nav.userNav {
padding: 1em 3em 1em 1em;
}
nav.userNav .userNavProfile {
text-align: center;
}
nav.userNav .userNavProfile::after {
content: "";
width: 10em;
display: inline-block;
height: 1px;
background-color: rgba(0, 0, 0, 0.5);;
}
nav.userNav .userNavProfile img {
width: 7em;
height: 7em;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: inline-block;
}
nav.userNav .userNavProfile .name {
font-size: 1.5em;
font-weight: bold;
color: #353535;
margin: .5em 1em 0 1em;
}
nav.userNav .links {
padding: .7em 0;
}
nav.userNav .links .navItem {
display: block;
margin: .3em .5em;
font-size: 1em;
color: #707070;
text-decoration: none;
}
nav.userNav .links .navItem:hover {
color: #505050;
font-weight: bold;
text-decoration: none;
}
nav.userNav .links .navItem.active {
color: #673ab7;
font-weight: bold;
}

View File

@ -0,0 +1,68 @@
section.bigCardWrapper.double {
display: flex;
flex-direction: row;
max-width: 80vw;
margin: auto;
}
section.bigCardWrapper.double > .bigCard {
flex: 1;
padding: 1em 6em;
margin: 0;
}
section.bigCardWrapper.double > .bigCard:first-child {
margin-right: 3em;
}
.listItem {
position: relative;
}
.listItem h2 {
font-size: 1rem;
color: #353535;
margin: 1em 0 0 0;
}
.listItem p {
margin: .2em 0 0 0;
}
.listItem.state1::before, .listItem.state2::before, .listItem.state3::before {
content: "";
position: absolute;
top: .2em;
left: -1.5em;
width: 1em;
height: 1em;
border-radius: 50%;
background-color: #f57c7c; /* state 1: no support */
}
.listItem.state2::before {
background-color: #e6ae65; /* state 2: partial support/bugged */
}
.listItem.state3::before {
background-color: #58ef81; /* state 3: full support/working */
}
@media screen and (max-width: 1000px) {
section.bigCardWrapper.double {
display: flex;
flex-direction: column;
}
section.bigCardWrapper.double > .bigCard {
flex: 0;
}
section.bigCardWrapper.double > .bigCard:first-child {
margin-right: 0;
margin-bottom: 3em;
}
}
@media screen and (max-width: 500px) {
section.bigCardWrapper.double {
max-width: 100vw;
margin: 0;
}
section.bigCardWrapper.double > .bigCard {
padding: 50px;
}
}

View File

@ -1,105 +0,0 @@
/*
pretendo.css -
main css file of the pretendo website
*/
/*
for home page only.
sets the background
*/
.hp-background {
-webkit-filter: brightness(30%) grayscale(100%);
filter: brightness(30%) grayscale(100%);
min-height: 100%;
min-width: 100%;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
}
/*
homepage container for the title and stuff
*/
.hp-container {
display: flex;
position: relative;
flex-direction: column;
justify-content: center;
text-align: left;
left: 0%;
z-index: 100;
}
/*
title text
*/
.title {
font-family: "Overpass Mono", monospace;
font-size: 500%;
line-height: 0;
color: white;
}
/*
main text
*/
.text {
font-family: "Space Mono", monospace;
font-size: 150%;
color: white;
}
/*
font awesome icon container stylings
*/
.icons {
margin-top: 10px;
font-size: 1.75em;
}
.icons > a:not(:first-child) > .icon, .icons > .icon:not(:first-child) {
margin-left: 20px;
}
.icon {
-webkit-transition: color 1s;
transition: color 1s;
color: white;
border: 0;
}
.icon:hover {
color: black;
}

Binary file not shown.

View File

@ -1,11 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="icomoon" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="pretendo-logo" d="M31.2 941.6l-27.2-18.4v-950.4l27.2-18.4c24-16.8 36.8-18.4 117.6-18.4h91.2v295.2c0 162.4 2.4 300.8 4.8 308 3.2 8.8 15.2 12.8 35.2 12.8 17.6 0 49.6 10.4 80 26.4 48 24 55.2 25.6 135.2 25.6 77.6 0 88.8-1.6 131.2-23.2 56.8-28.8 104-79.2 132-139.2 56-123.2-2.4-276.8-132-342.4-41.6-20.8-54.4-23.2-122.4-23.2-41.6 0-85.6 2.4-97.6 6.4l-22.4 5.6v-152h291.2c289.6 0 290.4 0 317.6 18.4l27.2 18.4v950.4l-54.4 36.8h-907.2l-27.2-18.4zM641.6 768c28-8.8 73.6-28.8 101.6-45.6 52-31.2 128.8-106.4 128.8-127.2 0-26.4-23.2-16.8-72.8 32-116.8 113.6-279.2 152.8-426.4 102.4-50.4-16.8-71.2-15.2-67.2 4.8 3.2 15.2 61.6 38.4 121.6 49.6 56.8 9.6 154.4 2.4 214.4-16zM596 692.8c22.4-5.6 59.2-20 82.4-32 48-24 121.6-91.2 121.6-111.2 0-26.4-18.4-22.4-54.4 12-52 49.6-78.4 66.4-136 85.6-58.4 20-141.6 22.4-198.4 4.8-52.8-16-67.2-15.2-67.2 3.2 0 14.4 36 33.6 84 43.2 35.2 8 130.4 4.8 168-5.6zM444 484.8c-30.4-13.6-72.8-58.4-83.2-87.2-4.8-12.8-8.8-41.6-8.8-64.8 0-89.6 73.6-154.4 168-147.2 92.8 6.4 149.6 72.8 142.4 167.2-4 52-16.8 75.2-65.6 114.4-24 20-37.6 24.8-79.2 26.4-30.4 1.6-58.4-1.6-73.6-8.8z" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

26
assets/js/auth.js Normal file
View File

@ -0,0 +1,26 @@
function postAjax(url, data, success) {
var params = (typeof data == 'string') ? data : Object.keys(data).map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
}).join('&');
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
xhr.open('POST', url);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(params);
return xhr;
}
function login() {
var xhr = postAjax('/api/v1/login', {
email: document.getElementById('email_input').value,
password: document.getElementById('password_input').value
});
xhr.onload = function () {
console.log(xhr.responseText);
response = JSON.parse(xhr.responseText);
if (response.success === true) {
location.pathname = '/pnid/dashboard';
}
}
}

View File

@ -1,28 +0,0 @@
/* eslint-env browser */
/*
index.js -
the main javascript file
*/
// set some variables
const bg = document.getElementById('hp-bg');
let index = 0;
const imageList = [ '/assets/images/bg1.gif', '/assets/images/bg2.gif' ];
// set the first image
bg.src = imageList[index];
index++;
setInterval(() => {
if (index === imageList.length) {
index = 0;
}
bg.src = imageList[index];
index++;
}, 5000);

3
assets/js/nav.js Normal file
View File

@ -0,0 +1,3 @@
document.getElementById('navToggle').onclick = () => {
document.getElementById('nav').classList.toggle('open');
};

View File

@ -12,5 +12,9 @@
},
"secrets": {
"session": "session secret here"
},
"recaptcha": {
"siteKey": "abcd",
"secretKey": "1234"
}
}

View File

@ -6,10 +6,26 @@ small commonly used utilities
*/
// shows 404 template. takes express response object
function sendDefault404(res) {
function send404(res) {
res.status(404).send('404');
}
function templateReadyUser(req) {
// normal user logged in
const isLoggedIn = req.user ? req.user.role != 'admin' : false;
const user = req.user;
return {
isLoggedIn,
user
};
}
function getLocales() {
return require('../locales/US_en.json');
}
module.exports = {
sendDefault404
send404,
templateReadyUser,
getLocales
};

View File

@ -1,28 +0,0 @@
/*
admin-authentication.js -
Middleware file for authentication checking
*/
// imports
const apiHelper = require('../helpers/api');
// middleware to use if admin authentication is required
function adminAuthNeeded(req, res, next) {
if (req.isAuthenticated() && req.user.role && req.user.role === 'admin') {
return next();
} else {
apiHelper.sendApiAuthError(res);
}
}
// middleware to use if authentication is optional
function authOptional(req, res, next) {
return next();
}
module.exports = {
adminAuthNeeded,
authOptional
};

View File

@ -0,0 +1,48 @@
/*
admin-authentication.js -
Middleware file for authentication checking
*/
// imports
const apiHelper = require('../helpers/api');
// middleware to use if admin authentication is required
function adminAuthNeeded(req, res, next) {
if (req.isAuthenticated() && req.user.role && req.user.role === 'admin') {
return next();
} else {
apiHelper.sendApiAuthError(res);
}
}
// middleware to use if pnid auth authentication is required
function pnidApiAuthNeeded(req, res, next) {
if (req.isAuthenticated() && !req.user.role) { // having no role means its a normal account. only admin accounts have roles
return next();
} else {
apiHelper.sendApiAuthError(res);
}
}
// middleware to use if pnid authentication is required
function pnidAuthNeeded(req, res, next) {
if (req.isAuthenticated() && !req.user.role) { // having no role means its a normal account. only admin accounts have roles
return next();
} else {
res.redirect('/pnid/login');
}
}
// middleware to use if authentication is optional
function authOptional(req, res, next) {
return next();
}
module.exports = {
adminAuthNeeded,
pnidAuthNeeded,
pnidApiAuthNeeded,
authOptional
};

View File

@ -45,7 +45,7 @@ const blogPostSchema = new mongoose.Schema({
date: {
type: Date,
default: () => {
return new Date(moment(new Date(), 'YYYY-MM-DD'));
return moment(moment().format('YYYY-MM-DD'));
}
},
category: {
@ -72,7 +72,7 @@ blogPostSchema.methods.postShortTemplate = function() {
return {
content: this.short,
title: this.name,
url: moment(this.meta.date, 'YYYY-MM-DD') + '/' + this.meta.slug
url: moment(this.meta.date).format('YYYY-MM-DD') + '/' + this.meta.slug
};
};
@ -85,13 +85,13 @@ blogPostSchema.statics.getPost = function(date, slug, callback) {
'meta.slug': slug
}, callback);
};
// not tested
blogPostSchema.statics.latestPostsShortTemlate = function(amount, callback) {
blogPostModel.find({}).sort({'meta.date': 'desc'}).exec(function(err, posts) {
if (err) return callback(err);
let out = [];
for (let i = 0, l = posts.length; i < ( amount+1 < l ? amount+1 : l); i++)
out += posts[i].postShortTemplate();
const out = [];
for (let i = 0, l = posts.length; i < ( amount+1 < l ? amount : l); i++)
out.push(posts[i].postShortTemplate());
callback(err, out);
});
};

118
models/pnid.js Normal file
View File

@ -0,0 +1,118 @@
/*
pnid.js -
file containing the model for pretendo network id's
*/
// imports
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const bcrypt = require('bcrypt');
// admin user database layout
const PNIDSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required.'],
unique: true,
trim: true,
validate: [validateEmail, 'Please fill a valid email address']
},
email_validated: {
type: Boolean,
default: false
},
// non hashed, gets hashed at save
password: {
type: String,
required: [true, 'Password is required.'],
minlength: [7, 'Password must be at-least 7 characters.'],
maxlength: [256, 'Password cannot be more than 256 characters long.'],
trim: true
},
pnid: {
key: {
type: String // not sure what this should be
},
pid: {
type: String,
unique: true
}
},
consoles: []
});
function validateEmail(email) {
// eslint throws "unnecesary character escape"
const re = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/; // eslint-disable-line
return re.test(email);
}
PNIDSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'});
// hashing password
PNIDSchema.pre('save', function(next) {
// only if modified
if (!this.isModified('password')) {
return next();
}
// hashing
const primaryhash = PNIDModel.hashPasswordPrimary(this.get('password'), this.get('pnid.pid'));
bcrypt.hash(primaryhash, 10, (err, hash) => {
if (err) {
return next(err);
}
this.set('password', hash);
next();
});
});
PNIDSchema.statics.findByEmail = function(email) {
return this.model('pnid').findOne({
email
});
};
PNIDSchema.statics.hashPasswordPrimary = function(password, pid) {
const buff1 = require('python-struct').pack('<I', pid);
const buff2 = Buffer.from(password).toString('ascii');
const unpacked = new Buffer(bufferToHex(buff1) + '\x02eCF' + buff2, 'ascii');
const hashed = require('crypto').createHash('sha256').update(unpacked).digest().toString('hex');
return hashed;
};
function bufferToHex(buff) {
let result = '';
const arr = buff.toString('hex').match(/.{1,2}/g);
for (let i=0;i<arr.length;i++) {
const char = arr[i];
result += String.fromCharCode(parseInt(char, 16));
}
result.replace(/\\/g, '&#92;');
return result;
}
PNIDSchema.statics.generatePID = async function() {
// Quick, dirty fix for PIDs
const pid = Math.floor(Math.random() * (4294967295 - 1000000000) + 1000000000);
const does_pid_inuse = await PNIDModel.findOne({
'pnid.pid': pid
});
if (does_pid_inuse) {
return '' + await PNIDModel.generatePID();
}
return '' + pid;
};
const PNIDModel = mongoose.model('pnid', PNIDSchema);
module.exports = {
PNIDModel,
PNIDSchema
};

18
package-lock.json generated
View File

@ -1186,6 +1186,11 @@
"promise": "7.3.1"
}
},
"express-recaptcha": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/express-recaptcha/-/express-recaptcha-4.0.3.tgz",
"integrity": "sha512-DUNGjloA7pDeTT//CP0Dv/gZmYU+Nv/ZL/KPHKsNknI2m1epFTc9BFR/oDdmsP1pklNJEZ4wQtDoeaiFYZK2hw=="
},
"express-session": {
"version": "1.15.6",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz",
@ -1650,6 +1655,11 @@
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
"long": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz",
"integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s="
},
"longest": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
@ -2172,6 +2182,14 @@
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
"dev": true
},
"python-struct": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/python-struct/-/python-struct-1.0.6.tgz",
"integrity": "sha512-r40v2MDFZKNBECMuo/oih6wInNEFXX0gmQgCJH4R7VULJsQDwcE95004r7A8iy1tDipgcZDyF8H6Tc2bhJdRZQ==",
"requires": {
"long": "3.2.0"
}
},
"qs": {
"version": "6.5.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",

View File

@ -21,12 +21,14 @@
"connect-mongo": "^2.0.1",
"express": "^4.16.2",
"express-handlebars": "^3.0.0",
"express-recaptcha": "^4.0.3",
"express-session": "^1.15.6",
"moment": "^2.22.2",
"mongoose": "^5.3.2",
"mongoose-unique-validator": "^2.0.2",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"python-struct": "^1.0.6",
"showdown": "^1.8.6"
},
"devDependencies": {

View File

@ -10,12 +10,14 @@ const LocalStrategy = require('passport-local').Strategy;
const passport = require('passport');
const bcrypt = require('bcrypt');
const adminUserModel = require('./models/admin-user').adminUserModel;
const PNIDModel = require('./models/pnid').PNIDModel;
// setup authentication
module.exports = (app) => {
app.use(passport.initialize());
app.use(passport.session());
passport.use('adminUserStrategy', new LocalStrategy(
(username, password, done) => {
// find user in database
adminUserModel.findByUsername(username).then((user) => {
@ -42,14 +44,57 @@ module.exports = (app) => {
});
}
));
passport.use('PNIDStrategy', new LocalStrategy({
usernameField: 'email'
}, (email, password, done) => {
// find user in database
PNIDModel.findByEmail(email).then((user) => {
if (!user) {
// user doesnt exist
return done(null, false, {message: 'Incorrect email'});
}
bcrypt.compare(password, user.password, (err, res) => {
if (err || !res) {
// error comparing hashes
// try primary hash on password and checking again.
bcrypt.compare(PNIDModel.hashPasswordPrimary(password, user.pnid.pid), user.password, (err, res) => {
if (err || !res) {
// error comparing hashes
// password hashed and non hashed both incorrect.
return done(null, false, {message: 'Incorrect password'});
}
// password is correct, return user
return done(null, user);
});
} else {
// password is correct, return user
return done(null, user);
}
});
}).catch((err) => {
if (err) {
// error finding in database
return done(null, false, {code: 500});
}
});
}
));
//Configuring app to have sessions, dont change since it would break everything
passport.serializeUser(function(user, done) {
done(null, user.id);
});
//SERIOUSLY. DONT TOUCH THIS SPAGHETTI, IT TOOK ME FOREVER TO GET THIS TO WORK!!!!1!!
passport.deserializeUser(function(id, done) {
adminUserModel.findById(id, function(err, user) {
done(err, user);
if (err || !user) {
PNIDModel.findById(id, function(err, user) {
done(err, user);
});
} else {
done(err, user);
}
});
});
};

View File

@ -10,7 +10,8 @@ const router = require('express').Router();
const passport = require('passport');
const moment = require('moment');
const apiHelper = require('../helpers/api');
const adminUserMiddleware = require('../middleware/admin-authentication');
const utilHelper = require('../helpers/util');
const userMiddleware = require('../middleware/authentication');
// database models
const adminUser = require('../models/admin-user');
@ -45,6 +46,7 @@ router.get('/admin', (req, res) => {
router.post('/admin/api/v1/login', passport.authenticate('adminUserStrategy'), function (req, res) {
apiHelper.sendReturn(res, {
username: req.user.username,
locales: utilHelper.getLocales(),
role: req.user.role ? req.user.role : undefined
});
});
@ -67,7 +69,7 @@ router.post('/admin/api/v1/login', passport.authenticate('adminUserStrategy'), f
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthNeeded, (req, res) => {
router.post('/admin/api/v1/register', userMiddleware.adminAuthNeeded, (req, res) => {
if (!req.body) {
// no post body
apiHelper.sendApiGenericError(res);
@ -81,10 +83,10 @@ router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthNeeded, (req,
});
// saving to database
newUser.save().then(() => {
newUser.save().then((user) => {
apiHelper.sendReturn(res, {
username: req.user.username,
role: req.user.role ? req.user.role : undefined
username: user.username,
role: user.role ? user.role : undefined
});
return;
}).catch((rejection) => {
@ -109,7 +111,7 @@ router.post('/admin/api/v1/register', adminUserMiddleware.adminAuthNeeded, (req,
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/removeadmin', adminUserMiddleware.adminAuthNeeded, (req, res) => {
router.post('/admin/api/v1/removeadmin', userMiddleware.adminAuthNeeded, (req, res) => {
if (!req.body) {
// no post body
apiHelper.sendApiGenericError(res);
@ -136,7 +138,7 @@ router.post('/admin/api/v1/removeadmin', adminUserMiddleware.adminAuthNeeded, (r
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/listadmins', adminUserMiddleware.adminAuthNeeded, (req, res) => {
router.get('/admin/api/v1/listadmins', userMiddleware.adminAuthNeeded, (req, res) => {
adminUser.adminUserModel.find({}, (err, admins) => {
// TODO format exception so it doesnt have a huge list of errors
if (err) return apiHelper.sendApiError(res, 500, [err]);
@ -167,7 +169,7 @@ router.get('/admin/api/v1/listadmins', adminUserMiddleware.adminAuthNeeded, (req
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/check', adminUserMiddleware.authOptional, (req, res) => {
router.get('/admin/api/v1/check', userMiddleware.authOptional, (req, res) => {
apiHelper.sendReturn(res, {
isAuthed: req.user ? true : false,
role: req.user ? (req.user.role ? req.user.role : undefined) : undefined
@ -185,7 +187,7 @@ router.get('/admin/api/v1/check', adminUserMiddleware.authOptional, (req, res) =
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/logout', adminUserMiddleware.adminAuthNeeded, (req, res) => {
router.get('/admin/api/v1/logout', userMiddleware.adminAuthNeeded, (req, res) => {
req.logout();
apiHelper.sendReturn(res, {});
});
@ -210,7 +212,7 @@ router.get('/admin/api/v1/logout', adminUserMiddleware.adminAuthNeeded, (req, re
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newpost', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/newpost', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);
@ -233,7 +235,7 @@ router.post('/admin/api/v1/newpost', adminUserMiddleware.adminAuthNeeded, functi
// saving post to database
newBlogPost.save().then((post) => {
apiHelper.sendReturn(res, {
url: moment(post.meta.date, 'YYYY-MM-DD') + '/' + post.meta.slug
url: moment(post.meta.date).format('YYYY-MM-DD') + '/' + post.meta.slug
});
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
@ -262,7 +264,7 @@ router.post('/admin/api/v1/newpost', adminUserMiddleware.adminAuthNeeded, functi
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editpost', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/editpost', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);
@ -299,7 +301,7 @@ router.post('/admin/api/v1/editpost', adminUserMiddleware.adminAuthNeeded, funct
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newauthor', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/newauthor', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);
@ -340,7 +342,7 @@ router.post('/admin/api/v1/newauthor', adminUserMiddleware.adminAuthNeeded, func
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editauthor', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/editauthor', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);
@ -377,7 +379,7 @@ router.post('/admin/api/v1/editauthor', adminUserMiddleware.adminAuthNeeded, fun
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newprogress', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/newprogress', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);
@ -429,7 +431,7 @@ router.post('/admin/api/v1/newprogress', adminUserMiddleware.adminAuthNeeded, fu
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editprogress', adminUserMiddleware.adminAuthNeeded, function (req, res) {
router.post('/admin/api/v1/editprogress', userMiddleware.adminAuthNeeded, function (req, res) {
if (!req.body) return apiHelper.sendApiGenericError(res);

View File

@ -7,6 +7,7 @@ file for handling routes regarding blog posts.
// imports
const router = require('express').Router();
const moment = require('moment');
const apiHelper = require('../helpers/api');
const utilHelper = require('../helpers/util');
const blogPostModel = require('../models/blog-post').blogPostModel;
@ -17,7 +18,7 @@ router.get('/news/:date/:title', (req, res) => {
// date format YYYY-MM-DD
if (/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(req.params.date) && /([a-z]|[0-9]|-)+/.test(req.params.title.toLowerCase())) {
// params are correct format
blogPostModel.getPost(new Date(req.params.date), req.params.title.toLowerCase(), (err, post) => {
blogPostModel.getPost(moment(req.params.date), req.params.title.toLowerCase(), (err, post) => {
// error exists or no post exists with the date and name
if (err || !post) {
console.log('error: ' + err + ' and post: ' + post);
@ -28,7 +29,9 @@ router.get('/news/:date/:title', (req, res) => {
post.postTemplate((err, postTemplate) => {
if (err) return utilHelper.send404(res);
res.render('post', {
post: postTemplate
post: postTemplate,
user: utilHelper.templateReadyUser(req),
locales: utilHelper.getLocales()
});
});
});
@ -53,7 +56,10 @@ router.get('/news', (req, res) => {
}
res.render('post-collection', {
posts: postCollection
posts: postCollection,
user: utilHelper.templateReadyUser(req),
locales: utilHelper.getLocales(),
page: 'news'
});
});
});

View File

@ -8,12 +8,17 @@ file for handling routes regarding contact
// imports
const router = require('express').Router();
const apiHelper = require('../helpers/api');
const utilHelper = require('../helpers/util');
const config = require('../config.json');
const https = require('https');
// display contact page
router.get('/contact', (req, res) => {
res.render('contact');
res.render('contact', {
user: utilHelper.templateReadyUser(req),
locales: utilHelper.getLocales(),
page: 'contact'
});
});
/*

View File

@ -8,10 +8,20 @@ for routes on the root path
// imports
const router = require('express').Router();
const utilHelper = require('../helpers/util');
const blogPostModel = require('../models/blog-post').blogPostModel;
// display home page
router.get('/', (req, res) => {
res.render('home');
// needs callback because mongoose is inconsistent
blogPostModel.latestPostsShortTemlate(2, (err, result) => {
res.render('home', {
user: utilHelper.templateReadyUser(req),
locales: utilHelper.getLocales(),
posts: result,
page: 'home'
});
});
});
// export the router

130
routes/pnid.js Normal file
View File

@ -0,0 +1,130 @@
/*
admin.js -
file for handling admin api.
*/
// imports
const router = require('express').Router();
const passport = require('passport');
const userMiddleware = require('../middleware/authentication');
const apiHelper = require('../helpers/api');
const utilHelper = require('../helpers/util');
const config = require('../config.json');
const Recaptcha = require('express-recaptcha').Recaptcha;
const recaptcha = new Recaptcha(config.recaptcha.siteKey, config.recaptcha.secretKey);
// database models
const PNID = require('../models/pnid');
// renders register page
router.get('/pnid/register', recaptcha.middleware.render, (req, res) => {
res.render('register', {
captcha: res.recaptcha,
locales: utilHelper.getLocales()
});
});
// renders login page
router.get('/pnid/login', (req, res) => {
res.render('login', {
locales: utilHelper.getLocales()
});
});
// logout
router.get('/pnid/logout', userMiddleware.pnidAuthNeeded, (req, res) => {
req.logout();
res.redirect('/');
});
// renders pnid dashboard
router.get('/pnid/dashboard', userMiddleware.pnidAuthNeeded, (req, res) => {
res.render('dashboard', {
locales: utilHelper.getLocales(),
user: utilHelper.templateReadyUser(req)
});
});
/*
* /api/v1/login
*
* signs user in
*
* post {
* email
* password
* }
* return {
* code: http code
* success: boolean
* username: undefined | string - only if login was successfull
* role: undefined | string - role of user if login was successfull
* errors: Strings[messages] - not yet :(
* }
*/
// TODO make login somehow display errors in correct format.
// middleware does the authentication work. this just returns a success
router.post('/api/v1/login', passport.authenticate('PNIDStrategy'), function (req, res) {
apiHelper.sendReturn(res, {
email: req.user.email,
email_validated: req.user.email_validated,
pnid: req.user.pnid.key
});
});
/*
* /api/v1/register
*
* registers a new admin user
*
* post {
* username
* password
* }
* return {
* code: httpcode
* success: boolean - true if register was successull
* username: undefined | string - username if register was successfullW
* errors: Strings[messages]
* }
*/
router.post('/api/v1/register', recaptcha.middleware.verify, async (req, res) => {
if (!req.body) {
// no post body
apiHelper.sendApiGenericError(res);
return;
}
/*if (req.recaptcha.error) {
apiHelper.sendApiError(res, 500, ['Captcha error']);
return;
}*/
const { email, password } = req.body;
const newUser = new PNID.PNIDModel({
email,
password,
pnid: {
key: 'abcd',
pid: await PNID.PNIDModel.generatePID()
}
});
// TODO verify password
// saving to database
newUser.save().then((user) => {
apiHelper.sendReturn(res, {
email: user.email,
email_validated: user.email_validated,
pnid: user.pnid.key
});
return;
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
console.log(rejection);
apiHelper.sendApiError(res, 500, [rejection]);
return;
});
});
// export the router
module.exports = router;

View File

@ -9,7 +9,6 @@ file for handling routes regarding progress
const router = require('express').Router();
const apiHelper = require('../helpers/api');
const utilHelper = require('../helpers/util');
const staticText = require('../static-text.json');
const progressListModel = require('../models/progress-list').progressListModel;
// display progress
@ -25,7 +24,9 @@ router.get('/progress', (req, res) => {
res.render('progress', {
games,
backends,
summary: staticText.progressSummary
user: utilHelper.templateReadyUser(req),
locales: utilHelper.getLocales(),
page: 'progress'
});
});
});

View File

@ -29,6 +29,7 @@ connection.on('error', console.error.bind(console, 'connection error:'));
// setup express
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({
secret: config.secrets.session,
@ -49,11 +50,22 @@ app.use(session({
passportconfig(app);
// handlebars templating setup
app.engine('.hbs', handlebars({
const hbsEngine = handlebars({
extname: '.hbs',
layoutsDir: 'views',
partialsDir: 'views/partials'
}));
partialsDir: 'views/partials',
helpers: {
'if_eq': function(a, b, opts) {
if(a == b) return opts.fn(this);
else return opts.inverse(this);
},
'if_neq': function(a, b, opts) {
if(a != b) return opts.fn(this);
else return opts.inverse(this);
}
}
});
app.engine('.hbs', hbsEngine);
app.set('view engine', '.hbs');
// locations and routes setup
@ -62,7 +74,8 @@ const locations = {
posts: require('./routes/blog'),
admin: require('./routes/admin'),
contact: require('./routes/contact'),
progress: require('./routes/progress')
progress: require('./routes/progress'),
pnid: require('./routes/pnid')
};
// static files
@ -73,6 +86,7 @@ app.use('/', locations.contact);
app.use('/', locations.posts);
app.use('/', locations.admin);
app.use('/', locations.progress);
app.use('/', locations.pnid);
app.use((req, res) => {
utilHelper.send404(res);
});

View File

@ -1,8 +1,18 @@
const mongoose = require('mongoose');
// fix database deprecation warnings
mongoose.set('useCreateIndex', true);
mongoose.set('useNewUrlParser', true);
// setup database connection
mongoose.connect(require('./config.json').database.url);
const connection = mongoose.connection;
connection.on('error', console.error.bind(console, 'connection error:'));
const adminUser = require('./models/admin-user');
const newUser = new adminUser.adminUserModel({
username: 'admin',
password: 'root'
password: 'rootroot'
});
newUser.save().then(() => {

View File

@ -1,127 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head }}
{{> head-common }}
</head>
<body>
{{> navbar }}
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<h1 class="txt-title">login</h1>
<form action="/admin/api/v1/login" method="POST">
<input type="text" name="username">
<input type="text" name="password">
<button>submit login</button>
</form>
<h1>Admin panel here</h1>
<h2>register</h2>
<form action="/admin/api/v1/register" method="POST">
<input type="text" name="username">
<input type="text" name="password">
<button>submit register</button>
</form>
<h2>login</h2>
<form action="/admin/api/v1/login" method="POST">
<input type="text" name="username">
<input type="text" name="password">
<button>submit login</button>
</form>
<div id="container">
<h1 class="txt-title">register new admin</h1>
<form action="/admin/api/v1/register" method="POST">
<input type="text" name="username">
<input type="text" name="password">
<button>submit register</button>
</form>
<h2>delete admin</h2>
<form action="/admin/api/v1/removeadmin" method="POST">
<p>id</p>
<input type="text" name="id">
<button>delete admin</button>
</form>
<h1 class="txt-title">delete admin</h1>
<form action="/admin/api/v1/removeadmin" method="POST">
<p>id</p>
<input type="text" name="id">
<button>delete admin</button>
</form>
<h2>create blog post</h2>
<form action="/admin/api/v1/newpost" method="POST">
<p>content</p>
<textarea name="content" cols="30" rows="10"></textarea>
<p>short version</p>
<textarea name="short" cols="30" rows="10"></textarea>
<p>category</p>
<input type="text" name="category">
<p>title</p>
<input type="text" name="title">
<p>author id</p>
<input type="text" name="author">
<button>create post</button>
</form>
<h1 class="txt-title">new blogpost</h1>
<form action="/admin/api/v1/newpost" method="POST">
<p>content</p>
<textarea name="content" cols="30" rows="10"></textarea>
<p>short version</p>
<textarea name="short" cols="30" rows="10"></textarea>
<p>category</p>
<input type="text" name="category">
<p>title</p>
<input type="text" name="title">
<p>author id</p>
<input type="text" name="author">
<button>create post</button>
</form>
<h2>edit blog post</h2>
<form action="/admin/api/v1/editpost" method="POST">
<p>content</p>
<textarea name="content" cols="30" rows="10"></textarea>
<p>short version</p>
<textarea name="short" cols="30" rows="10"></textarea>
<p>id</p>
<input type="text" name="id">
<p>category</p>
<input type="text" name="category">
<p>title</p>
<input type="text" name="title">
<p>author id</p>
<input type="text" name="author">
<button>edit post</button>
</form>
<h1 class="txt-title">edit blogpost</h1>
<form action="/admin/api/v1/editpost" method="POST">
<p>content</p>
<textarea name="content" cols="30" rows="10"></textarea>
<p>short version</p>
<textarea name="short" cols="30" rows="10"></textarea>
<p>id</p>
<input type="text" name="id">
<p>category</p>
<input type="text" name="category">
<p>title</p>
<input type="text" name="title">
<p>author id</p>
<input type="text" name="author">
<button>edit post</button>
</form>
<h2>new author</h2>
<form action="/admin/api/v1/newauthor" method="POST">
<p>name</p>
<input type="text" name="name">
<p>description</p>
<input type="text" name="description">
<p>image link</p>
<input type="text" name="image">
<button>submit author</button>
</form>
<h1 class="txt-title">new blogpost author</h1>
<form action="/admin/api/v1/newauthor" method="POST">
<p>name</p>
<input type="text" name="name">
<p>description</p>
<input type="text" name="description">
<p>image link</p>
<input type="text" name="image">
<button>submit author</button>
</form>
<h2>edit author</h2>
<form action="/admin/api/v1/editauthor" method="POST">
<p>id</p>
<input type="text" name="id">
<p>name</p>
<input type="text" name="name">
<p>description</p>
<input type="text" name="description">
<p>image link</p>
<input type="text" name="image">
<button>edit author</button>
</form>
<h1 class="txt-title">edit author info</h1>
<form action="/admin/api/v1/editauthor" method="POST">
<p>id</p>
<input type="text" name="id">
<p>name</p>
<input type="text" name="name">
<p>description</p>
<input type="text" name="description">
<p>image link</p>
<input type="text" name="image">
<button>edit author</button>
</form>
<h2>new progress entry</h2>
<form action="/admin/api/v1/newprogress" method="POST">
<p>title</p>
<input type="text" name="title">
<p>description</p>
<input type="text" name="description">
<p>state</p>
<p>0 - backend service<br>1 - no support<br>2 - partial support<br>3 - fully working</p>
<input type="text" name="state">
<button>submit progress</button>
</form>
<h1 class="txt-title">new progress entry</h1>
<form action="/admin/api/v1/newprogress" method="POST">
<p>title</p>
<input type="text" name="title">
<p>description</p>
<input type="text" name="description">
<p>state</p>
<p>0 - backend service<br>1 - no support<br>2 - partial support<br>3 - fully working</p>
<input type="text" name="state">
<button>submit progress</button>
</form>
<h2>edit progress entry</h2>
<form action="/admin/api/v1/editprogress" method="POST">
<p>id</p>
<input type="text" name="id">
<p>title</p>
<input type="text" name="title">
<p>description</p>
<input type="text" name="description">
<p>state</p>
<p>0 - backend service<br>1 - no support<br>2 - partial support<br>3 - fully working</p>
<input type="text" name="state">
<button>edit progress</button>
</form>
<h1 class="txt-title">edit progress entry</h1>
<form action="/admin/api/v1/editprogress" method="POST">
<p>id</p>
<input type="text" name="id">
<p>title</p>
<input type="text" name="title">
<p>description</p>
<input type="text" name="description">
<p>state</p>
<p>0 - backend service<br>1 - no support<br>2 - partial support<br>3 - fully working</p>
<input type="text" name="state">
<button>edit progress</button>
</form>
<br>
<a href="/admin/api/v1/listadmins">List of admin users</a>
<br>
<a href="/api/v1/listblog">List of blog posts</a>
<br>
<a href="/api/v1/listprogress">List of progress</a>
<br>
<a href="/api/v1/listauthors">List of authors</a>
<br>
<a href="/admin/api/v1/check">Check if logged in</a>
<br>
<a href="/admin/api/v1/logout">Logout</a>
{{> footer }}
<br>
<a href="/admin/api/v1/listadmins">List of admin users</a>
<br>
<a href="/api/v1/listblog">List of blog posts</a>
<br>
<a href="/api/v1/listprogress">List of progress</a>
<br>
<a href="/api/v1/listauthors">List of authors</a>
<br>
<a href="/admin/api/v1/check">Check if logged in</a>
<br>
<a href="/admin/api/v1/logout">Logout</a>
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@ -1,21 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head }}
<body>
{{> navbar }}
<h1>contact form</h1>
<form action="/api/v1/sendmessage" method="POST">
<p>content</p>
<textarea name="message" cols="30" rows="10"></textarea>
<p>subject</p>
<input type="text" name="subject">
<p>email</p>
<input type="text" name="email">
<button>send message</button>
</form>
{{> footer }}
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-contact.css">
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">{{ locales.contact.title }}</h1>
<form action="/api/v1/sendmessage" method="POST">
<div class="input-container">
<input type="text" name="email" placeholder="{{ locales.contact.email_placeholder }}">
<input type="text" name="subject" placeholder="{{ locales.contact.subject_placeholder }}">
</div>
<textarea name="message" placeholder="{{ locales.contact.message_placeholder }}"></textarea>
<div class="txt-center">
<button class="btn">{{ locales.contact.button_text }}</button>
</div>
</form>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

19
views/dashboard.hbs Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-pnid.css">
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper userPage">
{{> nav-dashboard }}
<section class="contentCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title small">ERROR</h1>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@ -1,46 +1,103 @@
<!--
home.hbs -
the front page of the pretendo website
-->
<!DOCTYPE html>
<html lang="en">
<head>
{{> head }}
<title>pretendo</title>
</head>
<body class="hp-container">
<img id="hp-bg" class="hp-background" src="">
<div class="hp-container">
<h1 class="title"><i class="pretendo-icon-pretendo-logo"></i>pretendo</h1>
<div class="text">
an accurate-as-possible server emulation project for<br>
nintendo consoles that use nintendo network
</div>
<div class="icons">
<a href="https://github.com/PretendoNetwork"><i class="fab fa-github-alt icon"></i></a>
<a href="https://www.npmjs.com/search?q=%40pretendo"><i class="fab fa-npm icon"></i></a>
<a href="https://discord.gg/rxekqVJ"><i class="fab fa-discord icon"></i></a>
<a href="https://twitter.com/pretendonetwork"><i class="fab fa-twitter icon"></i></a>
</div>
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-home.css">
</head>
<body>
{{> nav-default }}
<div class="contentWrapper">
<header>
<h1>ERROR</h1>
<hr>
<h3>ERROR</h3>
</header>
</div>
<div class="contentWrapper bgLines">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">{{ locales.about.title }}</h1>
<p>{{ locales.about.text }}</p>
</div>
</section>
</body>
<script src="/assets/js/index.js" type="text/javascript"></script>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">{{ locales.credits.title }}</h1>
<div class="cardProfiles">
<!-- profile -->
<div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div><!--
--><div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div><!--
--><div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div><!--
--><div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div><!--
--><div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div><!--
--><div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<div class="profileText">
<h2 class="txt-smallHeading">RedDucks</h2>
<p>Project owner, lead developer, very violent young man. Splatoon obsessed as well.</p>
</div>
</div>
</div>
</div>
</section>
<section class="bigCardWrapper">
<div class="bigCard lessPadding cont-flex">
<div class="flex-item">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">ERROR</h1>
<p>ERROR</p>
<button class="btn">ERROR</button>
</div>
<div class="cont-flex">
{{#each posts }}
<div class="smallCardWidth">
<div class="smallCard">
<h3 class="txt-smallHeading">{{ this.title }}</h3>
<p>{{ this.content }}</p>
<a href="/news/{{ this.url }}" class="btn-alt">ERROR</a>
</div>
</div>
{{/each }}
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

27
views/login.hbs Normal file
View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src="/assets/js/auth.js" defer></script>
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locales.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title small">{{ locales.login_card.title }}</h1>
<form action="/api/v1/sendmessage" method="POST">
<label for="email">{{ locales.login_card.email_text }}</label>
<input type="text" class="hasLabel" id="email_input" name="email">
<label for="email">{{ locales.login_card.password_text }}</label>
<input type="password" class="hasLabel" id="password_input" name="password">
</form>
<button class="btn btnMargin" onclick="login()">{{ locales.login_card.button_text }}</button>
<p class="lowText">ERROR <a href="#">ERROR</a></p>
</div>
</div>
{{> footer-default }}
</body>
</html>

View File

@ -0,0 +1,5 @@
<footer>
<div class="content">
<p>{{ locales.footer.text }}</p>
</div>
</footer>

View File

@ -1,3 +0,0 @@
<footer class="footer">
<p>&copy; 2018 Pretendo</p>
</footer>

View File

@ -0,0 +1,6 @@
{{> metatags }}
<title>ERROR</title>
<!-- css files -->
<link rel="stylesheet" href="/assets/css/pretendo-common.css">
<!-- scripts -->
<script src="/assets/js/nav.js" defer></script>

View File

@ -1,14 +0,0 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="an accurate-as-possible server emulation project for nintendo network-enabled devices">
<link rel="icon" href="/assets/favicon.ico">
<link rel="apple-touch-icon" href="/assets/favicon.ico">
<link rel="mask-icon" href="/assets/images/pretendo-logo.svg">
<link href="https://fonts.googleapis.com/css?family=Overpass+Mono|Space+Mono" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/pretendo.css">
<link rel="stylesheet" href="/assets/css/icons.css">
<script defer src="https://use.fontawesome.com/releases/v5.0.10/js/all.js" integrity="sha384-slN8GvtUJGnv6ca26v8EzVaR9DC58QEwsIk9q1QXdCU8Yu8ck/tL/5szYlBbqmS+" crossorigin="anonymous"></script>

View File

@ -0,0 +1,4 @@
<!-- meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">

View File

@ -0,0 +1,12 @@
<nav class="userNav">
<div class="userNavProfile">
<img src="" alt="">
<p class="name">Mrjvs</p>
</div>
<div class="links">
<a class="navItem active" href="#">ERROR</a>
<a class="navItem" href="#">ERROR</a>
<a class="navItem" href="#">ERROR</a>
<a class="navItem" href="#">ERROR</a>
</div>
</nav>

View File

@ -0,0 +1,18 @@
<nav class="navWrapper" id="nav">
<a class="navItem brand{{#if_eq page 'home'}} active{{/if_eq}}" {{#if_neq page 'home'}}href="/"{{/if_neq}}>{{ locales.nav.brand }}</a>
<a class="navItem{{#if_eq page 'news'}} active{{/if_eq}}" {{#if_neq page 'news'}}href="/news"{{/if_neq}}>{{ locales.nav.links.news }}</a>
<a class="navItem{{#if_eq page 'progress'}} active{{/if_eq}}" {{#if_neq page 'progress'}}href="/progress"{{/if_neq}}>{{ locales.nav.links.progress }}</a>
<a class="navItem{{#if_eq page 'contact'}} active{{/if_eq}}" {{#if_neq page 'contact'}}href="/contact"{{/if_neq}}>{{ locales.nav.links.contact }}</a>
<div class="navSpread"></div>
{{#if user.isLoggedIn }}
<a class="navItem navProfile" href="/pnid/dashboard">
<img src="#" alt="">
<p>mrjvs@gmail.com</p>
</a>
<a class="navItem navBtn" href="/pnid/logout">logout</a>
{{else}}
<a class="navItem navBtnAlt" href="/pnid/login">{{ locales.nav.links.login }}</a>
<a class="navItem navBtn" href="/pnid/register">{{ locales.nav.links.register }}</a>
{{/if}}
<a class="navItem hamburger" id="navToggle">ICON</a>
</nav>

View File

@ -1 +0,0 @@

View File

@ -1,7 +1,29 @@
<h1>Latest posts</h1>
{{#each posts }}
<strong>{{ this.title }}</strong>
<p>{{ this.content }}</p>
<a href="{{ this.url }}">link</a>
<br><br>
{{/each}}
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-news.css">
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard lessPadding">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">ERROR</h1><!--
{{#each posts }}
--><article class="smallCard">
<h3 class="txt-smallHeading">{{ this.title }}</h3>
<p>{{ this.content }}</p>
<a class="btn-alt" href="/news/{{ this.url }}">ERROR</a>
</article><!--
{{/each}}
-->
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@ -1,22 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head }}
<body>
{{> navbar }}
<h1>{{ post.title }}</h1>
<p>{{ post.date }}</p>
<p>{{ post.category }}</p>
<article>
{{{ post.content }}}
</article>
<p>{{ post.author.name }} - {{ post.author.description }}</p>
<img src="{{ post.author.profilePicture }}">
{{> footer }}
{{> head-common }}
<!-- page specific -->
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">{{ post.category }} &mdash; {{ post.date }}</p>
<h1 class="txt-title">{{ post.title }}</h1>
{{{ post.content }}}
<div class="profile">
<img class="profileIcon" src="{{ post.author.profilePicture }}">
<div class="profileText">
<h2 class="txt-smallHeading">{{ post.author.name }}</h2>
<p>{{ post.author.description }}</p>
</div>
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@ -1,15 +1,44 @@
<h1>progress summary</h1>
<p>{{ summary }}</p>
<br>
<h2>game support</h2>
{{#each games }}
<strong>{{ this.title }}</strong>
<p>{{ this.description }}</p>
<p>state: {{ this.state }}</p>
{{/each}}
<br><br>
<h2>backend support</h2>
{{#each backends }}
<strong>{{ this.title }}</strong>
<p>{{ this.description }}</p>
{{/each}}
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-progress.css">
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">ERROR</h1>
<p>{{ summary }}</p>
</div>
</section>
<section class="bigCardWrapper double">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">ERROR</h1>
{{#each games }}
<div class="listItem state{{ this.state }}">
<h2>{{ this.title }}</h2>
<p>{{ this.description }}</p>
</div>
{{/each}}
</div>
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<h1 class="txt-title">ERROR</h1>
{{#each backends }}
<div class="listItem">
<h2>{{ this.title }}</h2>
<p>{{ this.description }}</p>
</div>
{{/each}}
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

31
views/register.hbs Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src="/assets/js/auth.js" defer></script>
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locales.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">{{ locales.register_card.caption }}</p>
<h1 class="txt-title small">{{ locales.register_card.title }}</h1>
<form action="/api/v1/sendmessage" method="POST">
<label for="email">{{ locales.register_card.email_text }}</label>
<input type="text" class="hasLabel" id="email_input" name="email">
<label for="email">{{ locales.register_card.username_text }}</label>
<input type="text" class="hasLabel" id="username_input" name="username">
<label for="email">{{ locales.register_card.password_text }}</label>
<input type="password" class="hasLabel" id="password_input" name="password">
<label for="email">{{ locales.register_card.password_confirm_text }}</label>
<input type="password" class="hasLabel" id="password_confirm_input" name="confirm_password">
</form>
<button class="btn btnMargin" onclick="register()">ERROR</button>
<p class="lowText">ERROR <a href="#">ERROR</a></p>
</div>
</div>
{{> footer-default }}
</body>
</html>