Stripped down and reworked pages

Stripped down the website to remove account managment and API handling. These features may be brought back in the future when the network goes live. A few new pages such as the Cemu and FAQ pages were added along with a finished credits list
This commit is contained in:
Jonathan Barrow
2020-03-08 22:25:38 -04:00
parent ab65bdf950
commit 6d65ac3912
55 changed files with 1180 additions and 4964 deletions

View File

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

@@ -91,7 +91,8 @@ nav.navWrapper .navProfile {
}
nav.navWrapper .navProfile img {
border-radius: 50%;
width: 2em; height: 2em;
width: 2em;
height: 2em;
background-color: white;
border: 1px solid rgba(0, 0, 0, 0.5);
}
@@ -156,7 +157,8 @@ nav.navWrapper.open .navItem.hamburger {
/* content wrapper */
.contentWrapper {
position: relative;
width: 100%;
max-width: 100%;
overflow-x: hidden;
}
nav.navWrapper ~ .contentWrapper:nth-child(2) {
margin-top: 3rem;

View File

@@ -1,15 +0,0 @@
.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,29 @@
::-webkit-details-marker {
float: right;
margin-top: 3px;
}
details {
background: #f6f6f6;
border-radius: 7px;
margin-top: 20px;
cursor: pointer;
}
details[open] summary {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
details summary {
border-radius: 7px;
padding: 10px 20px;
background: #673db6;
color: white;
outline: none;
}
details .text {
margin-left: 10px;
padding-bottom: 10px;
}

View File

@@ -1,26 +0,0 @@
/* 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

@@ -1,176 +0,0 @@
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;
}
.infoContainer.hide {
display: none;
}
.infoList {
display: block;
padding: 1em 0;
font-weight: bold;
color: #000;
margin: 0;
}
.infoList span {
font-weight: normal;
display: inline-block;
color: #707070;
width: 6em;
}
.infoList + .infoList {
border-top: 1px solid #afafaf;
}
.infoContainer .btn {
margin-top: 1em;
}
.link-margin {
margin: 0 1em;
}
/* 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;
}
/* email popup */
.email_popup {
margin: 10px 4em;
margin-top: 55px;
background-color: white;
border: 1px solid #d1d1d1;
padding: 1em 2em;
transition: transform .2s ease-in, max-height .2s, padding .5s;
overflow: hidden;
max-height: 100vh;
}
.email_popup.closed {
transform: translateY(-15rem);
max-height: 0;
padding: 0;
}
.email_popup p {
padding: 0;
display: inline-block;
}
.email_popup span {
float: right;
}
.email_popup button {
margin: .4em;
}
@media screen and (max-width: 950px) {
body .contentWrapper.userPage {
flex-direction: column;
}
nav.userNav {
display: flex;
flex-direction: row;
justify-content: center;
}
.email_popup {
margin: 10px;
margin-top: 55px;
}
.contentCard {
display: block;
margin: auto;
}
nav.userNav .links {
display: flex;
flex-direction: row;
align-items: flex-end;
}
nav.userNav .links .navItem {
display: inline-block;
}
nav.userNav .userNavProfile::after {
display: none;
}
}
@media screen and (max-width: 500px) {
.contentCard {
width: 100vw;
}
}
@media screen and (max-width: 450px) {
nav.userNav {
display: flex;
flex-direction: column;
justify-content: center;
}
nav.userNav .links {
display: flex;
flex-direction: column;
align-items: center;
}
nav.userNav .userNavProfile::after {
display: inline-block;
}
}

View File

@@ -1,68 +0,0 @@
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,150 +0,0 @@
function postAjax(url, data, callbacks={}) {
const params = (typeof data == 'string') ? data : Object.keys(data).map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(data[k])
}).join('&');
const xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
for (const event in callbacks) {
const handler = callbacks[event];
if (handler instanceof Function) {
xhr.addEventListener(event, handler);
}
}
xhr.open('POST', url);
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send(data);
return xhr;
}
function showErrorPopup(errorText) {
console.log(errorText);
document.getElementById('errorBox').innerHTML = errorText;
document.getElementById('errorBox').classList.remove('hide');
}
function showSuccessPopup(successText) {
document.getElementById('successBox').innerHTML = successText;
document.getElementById('successBox').classList.remove('hide');
}
function login() {
var xhr = postAjax('/api/v1/login', {
email: document.getElementById('email_input').value,
password: document.getElementById('password_input').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
location.pathname = '/pnid/dashboard';
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function editInfo() {
var xhr = postAjax('/api/v1/editaccount', {
email: document.getElementById('email_input').value,
username: document.getElementById('username_input').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
location.pathname = '/pnid/dashboard';
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function passwordForgot() {
var xhr = postAjax('/api/v1/passwordforgot', {
email: document.getElementById('email_input').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
document.getElementById('email_input').value = '';
return showSuccessPopup('An email has been sent to your email address.');
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function passwordReset() {
var xhr = postAjax('/api/v1/passwordreset', {
password: document.getElementById('password_input').value,
password_confirm: document.getElementById('password_confirm').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
location.pathname = '/pnid/login';
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function register() {
var xhr = postAjax('/api/v1/register', {
email: document.getElementById('email_input').value,
username: document.getElementById('username_input').value,
password: document.getElementById('password_input').value,
confirm_password: document.getElementById('password_confirm_input').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
login();
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function sendMessage() {
var xhr = postAjax('/api/v1/sendmessage', {
email: document.getElementById('email_input').value,
subject: document.getElementById('subject_input').value,
message: document.getElementById('message_input').value
});
xhr.onload = function () {
if (xhr.responseText.charAt(0) !== "{") {
return showErrorPopup('Whoops, something went wrong. Try again later.');
}
response = JSON.parse(xhr.responseText);
if (response.success === true) {
document.getElementById('email_input').value = '';
document.getElementById('subject_input').value = '';
document.getElementById('message_input').value = '';
return showSuccessPopup('Your message has been sent.');
} else if (response.errors) {
showErrorPopup(response.errors);
}
}
}
function email_resend() {
var xhr = postAjax('/api/v1/resendemail', {});
document.getElementById('email_verification_popup').classList.add('closed');
}

View File

@@ -1,4 +0,0 @@
function toggleEditInfo() {
document.getElementById('userInfo').classList.toggle('hide');
document.getElementById('editInfo').classList.toggle('hide');
}

View File

@@ -1,27 +1,5 @@
{
"http": {
"port": 8080
},
"database": {
"url": "mongodb://localhost:27017/pretendo_website"
},
"contactWebhook": {
"port": 443,
"host": "host.com",
"path": "/webhookurl"
},
"secrets": {
"session": "session secret here"
},
"email": {
"service": "gmail",
"auth": {
"user": "example@email.com",
"pass": "password"
}
},
"recaptcha": {
"siteKey": "abcd",
"secretKey": "1234"
}
"http": {
"port": 8080
}
}

View File

@@ -1,71 +0,0 @@
/*
api.js -
common api returns
*/
/**
* Send generic API response
* @param {ServerResponse} response An express ServerResponse response
* @param {Object} data Response data
* @param {Array} errors Request errors
*/
function sendReturn(response, data, errors) {
return response.status(data.code || 200).json(
Object.assign({
code: 200,
success: true,
errors: [] + (errors ? errors : [])
}, data)
);
}
/**
* Send API 404
* @param {ServerResponse} response An express ServerResponse response
*/
function sendApi404(response) {
return sendApiError(response, 404, [
'Endpoint not in use'
]);
}
/**
* Send user not authenticated error
* @param {ServerResponse} response An express ServerResponse response
*/
function sendApiAuthError(response) {
return sendApiError(response, 401, [
'Not authenticated'
]);
}
/**
* Send a generic API error
* @param {ServerResponse} response An express ServerResponse response
*/
function sendApiGenericError(response) {
return sendApiError(response, 400, [
'Bad request'
]);
}
/**
* Send an API error
* @param {ServerResponse} response An express ServerResponse response
*/
function sendApiError(response, code, errors) {
return sendReturn(response, {
code,
success: false
}, errors);
}
module.exports = {
sendReturn,
sendApi404,
sendApiGenericError,
sendApiError,
sendApiAuthError
};

View File

@@ -1,31 +0,0 @@
const nodemailer = require('nodemailer');
const config = require('../config');
const transporter = nodemailer.createTransport(config.email);
/**
* Sends an email with the specified subject and message to an email address
* @param {String} email the destination email address
* @param {String} subject The Subject of the email
* @param {String} message The body of the email
*/
function send(email, subject = 'No email subject provided', message = 'No email body provided') {
const options = {
from: config.email.address,
to: email,
subject: subject,
html: message
};
transporter.sendMail(options, (error, info) => {
if (error) {
console.warn(error);
} else {
console.log(info);
}
});
}
module.exports = {
send: send
};

View File

@@ -6,38 +6,7 @@ small commonly used utilities
*/
const fs = require('fs-extra');
const logger = require('winston');
// shows 404 template. takes express response object
function send404(res) {
res.status(404).render('404');
}
function templateReadyUser(req) {
// normal user logged in
const isLoggedIn = req.user ? req.user.role != 'admin' : false;
const user = req.user ? req.user.toObject() : undefined;
return {
isLoggedIn,
user
};
}
// Returns a list of possible locales:
/*
* [
* {
* region: 'US',
* language: 'en',
* display: 'American English',
* flag_id: 1 // I dunno maybe we display a local list that also displays flags?
* }
* ]
*
*/
function getLocales() {
return [];
}
const logger = require('../logger');
// Returns a locale
function getLocale(region, language) {
@@ -47,7 +16,7 @@ function getLocale(region, language) {
return require(path);
}
logger.log('warn', `Could not find locale ${region}_${language}! Loading default`);
logger.warn(`Could not find locale ${region}_${language}! Loading default`);
return getDefaultLocale();
}
@@ -57,15 +26,7 @@ function getDefaultLocale(locale='default') {
return require(`${__dirname}/../locales/${locale}.json`);
}
function generateRandomInt(length = 4) {
return Math.floor(Math.pow(10, length-1) + Math.random() * 9 * Math.pow(10, length-1));
}
module.exports = {
send404,
templateReadyUser,
getLocales,
getLocale,
getDefaultLocale,
generateRandomInt
getDefaultLocale
};

View File

@@ -1,14 +1,6 @@
{
"nav": {
"brand": "Pretendo",
"links": {
"news": "News",
"progress": "Progress",
"contact": "Contact",
"login": "Login",
"register": "Register",
"logout": "Logout"
}
"brand": "Pretendo"
},
"home": {
"header": {
@@ -16,117 +8,16 @@
"text": "An open source Nintendo Network replacement service"
}
},
"progress": {
"summary": {
"caption": "All in one",
"title": "Progress Summary",
"text": "We should probably pull this from a database ngl"
},
"compatibility": {
"caption": "Useful info",
"title": "Game Compatibility"
},
"misc": {
"caption": "Less useful info",
"title": "Other"
}
},
"news": {
"caption": "Read up",
"title": "All news",
"button_text": "Read more"
},
"contact": {
"caption": "Have a question?",
"title": "Contact Us",
"button_text": "Send Message",
"email_placeholder": "email@provider.com",
"subject_placeholder": "Subject",
"message_placeholder": "Message"
},
"login": {
"card": {
"caption": "Sign in",
"title": "Login",
"email_text": "Username or Email",
"password_text": "Password",
"button_text": "Sign In",
"forgotpass_introduction": "Forgot password?",
"forgotpass_text": "Click here to reset it.",
"link_introduction": "Don't have an account yet?",
"link_text": "Click here to register."
}
},
"register": {
"card": {
"title": "Register",
"birthdate_text": "Birthday",
"gender_text": "Gender",
"email_text": "Email Address",
"username_text": "Username",
"password_text": "Password",
"password_confirm_text": "Confirm Password",
"caption": "Sign Up",
"button_text": "Sign up",
"link_introduction": "Already have an account?",
"link_text": "Click here to login."
}
},
"password": {
"card": {
"caption": "Forgot password?",
"title": "Reset password",
"password_text": "New password",
"password_confirm_text": "Confirm password",
"button_text": "Reset password"
}
},
"forgot": {
"card": {
"caption": "Did you forget something?",
"title": "Forgot password",
"email_text": "Email address",
"button_text": "Send email"
}
},
"about": {
"caption": "What is this?",
"title": "About",
"text": "Pretendo is an open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS/2DS family of consoles. Our goal is to preserve the online functionality of these consoles, to allow players to continue to play their favorite WiiU and 3DS/2DS games to their fullest capacity."
"text": "Pretendo is an open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles. Our goal is to preserve the online functionality of these consoles, to allow players to continue to play their favorite WiiU and 3DS games to their fullest capacity."
},
"credits": {
"caption": "Who are responsible?",
"caption": "The people responsible",
"title": "Credits"
},
"latest_news": {
"caption": "Catch up",
"title": "Latest news",
"description": "Pretendo is constantly evolving to create the best possible servers. We release posts to showcase what we have been working on and how far we got with certain things.",
"button_text": "All posts",
"news_button_text": "Read more"
},
"errors": {
"invalid_login": "Invalid username/password combination",
"account_exists": "An account with that username already exists!"
},
"verify_email_popup": {
"text": "Your email has not been verified yet. You are restricted from certain features.",
"change_button_text": "Change email",
"repeat_button_text": "Send again"
},
"dashboard": {
"navigation": {
"links": {
"account": "Account",
"soon": "Coming soon"
}
},
"account": {
"caption": "Pretendo Network ID",
"title": "Account information"
}
},
"footer": {
"text": "Copyright 2019 PretendoNetwork - Site designed and developed by MrJVS, ben and Jip"
"text": "Copyright 2020 PretendoNetwork - Site designed and developed by mrjvs, ben and jip"
}
}

View File

@@ -1,14 +1,6 @@
{
"nav": {
"brand": "ERROR",
"links": {
"news": "ERROR",
"progress": "ERROR",
"contact": "ERROR",
"login": "ERROR",
"register": "ERROR",
"logout": "ERROR"
}
"brand": "ERROR"
},
"home": {
"header": {
@@ -16,60 +8,15 @@
"text": "ERROR"
}
},
"progress": {
"summary": {
"title": "ERROR",
"text": "ERROR"
},
"compatability": {
"title": "ERROR"
},
"misc": {
"title": "ERROR"
}
},
"contact": {
"title": "ERROR",
"button_text": "ERROR",
"email_placeholder": "ERROR",
"subject_placeholder": "ERROR",
"message_placeholder": "ERROR"
},
"login": {
"card": {
"title": "ERROR",
"email_text": "ERROR",
"password_text": "ERROR",
"email_placeholder": "ERROR",
"password_placeholder": "ERROR",
"button_text": "Sign In"
}
},
"register": {
"card": {
"title": "ERROR",
"birthdate_text": "ERROR",
"gender_text": "ERROR",
"email_text": "ERROR",
"username_text": "ERROR",
"password_text": "ERROR",
"password_confirm_text": "ERROR",
"email_placeholder": "ERROR",
"password_placeholder": "ERROR",
"caption": "ERROR"
}
},
"about": {
"caption": "ERROR",
"title": "ERROR",
"text": "ERROR"
},
"credits": {
"caption": "ERROR",
"title": "ERROR"
},
"errors": {
"invalid_login": "ERROR",
"account_exists": "ERROR"
},
"footer": {
"text": "ERROR"
}

View File

@@ -1,41 +1,54 @@
const winston = require('winston');
const fs = require('fs-extra');
require('colors');
const logger = winston.createLogger({
level: 'verbose',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(log => {
return `${log.timestamp} | ${log.level}: ${log.message}`;
})
),
transports: [
new winston.transports.Console({ colorize: true }),
new winston.transports.File({
colorize: false,
json: false,
filename: `${__dirname}/logs/events.log`
}),
new winston.transports.File({
colorize: false,
json: false,
filename: `${__dirname}/logs/error.log`,
level: 'error'
}),
new winston.transports.File({
colorize: false,
json: false,
filename: `${__dirname}/logs/warn.log`,
level: 'warn'
}),
new winston.transports.File({
colorize: false,
json: false,
filename: `${__dirname}/logs/debug.log`,
level: 'debug'
})
]
});
class Logger {
constructor(root = '') {
fs.ensureDirSync(`${root}/logs`);
winston.add(logger);
this.root = root;
this.streams = {
latest: fs.createWriteStream(`${this.root}/logs/latest.log`),
success: fs.createWriteStream(`${this.root}/logs/success.log`),
error: fs.createWriteStream(`${this.root}/logs/error.log`),
warn: fs.createWriteStream(`${this.root}/logs/warn.log`),
info: fs.createWriteStream(`${this.root}/logs/info.log`)
};
}
module.exports = winston;
success(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
this.streams.success.write(`${input}\n`);
console.log(`${input}`.green.bold);
}
error(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
this.streams.error.write(`${input}\n`);
console.log(`${input}`.red.bold);
}
warn(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
this.streams.warn.write(`${input}\n`);
console.log(`${input}`.yellow.bold);
}
info(input) {
const time = new Date();
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
this.streams.info.write(`${input}\n`);
console.log(`${input}`.cyan.bold);
}
}
const logger = new Logger(__dirname);
module.exports = logger;

View File

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

@@ -1,69 +0,0 @@
/*
admin-user.js -
file containing the model file for admin user
*/
// imports
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const bcrypt = require('bcrypt');
// admin user database layout
const adminUserSchema = new mongoose.Schema({
username: {
type: String,
required: [true, 'Username is required.'],
minlength: [1, 'Username must be at-least 1 character.'],
maxlength: [42, 'Username cannot be more than 42 characters long.'],
unique: true,
trim: true
},
// 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
},
role: {
type: String,
required: true,
default: 'admin'
}
});
adminUserSchema.plugin(uniqueValidator, {message: '{PATH} already in use.'});
// hashing password
adminUserSchema.pre('save', function(next) {
// only if modified
if (!this.isModified('password')) {
return next();
}
// hashing
bcrypt.hash(this.get('password'), 10, (err, hash) => {
if (err) {
return next(err);
}
this.set('password', hash);
next();
});
});
adminUserSchema.statics.findByUsername = function(username) {
return this.model('adminUser').findOne({
username
});
};
const adminUserModel = mongoose.model('adminUser', adminUserSchema);
module.exports = {
adminUserModel,
adminUserSchema
};

View File

@@ -1,105 +0,0 @@
/*
blog-post.js -
file containing the model file for a blog post
*/
// imports
const mongoose = require('mongoose');
const postAuthor = require('./post-author').postAuthorModel;
const showdown = require('showdown');
const moment = require('moment');
const converter = new showdown.Converter();
converter.setFlavor('github');
// admin user database layout
const blogPostSchema = new mongoose.Schema({
// html in content
content: {
type: String,
required: [true, 'Content is required.'],
trim: true
},
// title of blog post
name: {
type: String,
required: [true, 'Name is required']
},
// short description of blog post
short: {
type: String,
required: [true, 'Short version is required']
},
meta: {
slug: {
type: String,
required: [true, 'Author is required'],
trim: true
},
author: {
type: String,
required: [true, 'Author is required'],
trim: true
},
date: {
type: Date,
default: () => {
return moment(moment().format('YYYY-MM-DD'));
}
},
category: {
type: String,
required: [true, 'category is required'],
trim: true
}
}
});
blogPostSchema.methods.postTemplate = function(callback) {
const self = this;
postAuthor.findById(this.meta.author, function (err, author) {
callback(err, {
content: self.content,
title: self.name,
date: self.meta.date,
dateString: moment(self.meta.date).format('YYYY-MM-DD'),
category: self.meta.category,
author: author.authorTemplate()
});
});
};
blogPostSchema.methods.postShortTemplate = function() {
return {
content: this.short,
title: this.name,
url: moment(this.meta.date).format('YYYY-MM-DD') + '/' + this.meta.slug
};
};
blogPostSchema.statics.markdownToHtml = function(markdown) {
return converter.makeHtml(markdown);
};
blogPostSchema.statics.getPost = function(date, slug, callback) {
return blogPostModel.findOne({
'meta.date': date,
'meta.slug': slug
}, callback);
};
blogPostSchema.statics.latestPostsShortTemlate = function(amount, callback) {
blogPostModel.find({}).sort({'meta.date': 'desc'}).exec(function(err, posts) {
if (err) return callback(err);
const out = [];
for (let i = 0, l = posts.length; i < ( amount+1 < l ? amount : l); i++)
out.push(posts[i].postShortTemplate());
callback(err, out);
});
};
const blogPostModel = mongoose.model('blogPost', blogPostSchema);
module.exports = {
blogPostModel,
blogPostSchema
};

View File

@@ -1,184 +0,0 @@
/*
pnid.js -
file containing the model for pretendo network id's
*/
// imports
const mongoose = require('mongoose');
const uniqueValidator = require('mongoose-unique-validator');
const randtoken = require('rand-token');
const bcrypt = require('bcrypt');
const utilHelper = require('../helpers/util');
// admin user database layout
const PNIDSchema = new mongoose.Schema({
email: {
type: String,
required: [true, 'Email is required.'],
unique: true,
trim: true,
uppercase: true,
validate: [validateEmail, 'Please fill a valid email address']
},
email_validated: {
type: Boolean,
default: false
},
email_validation_code: {
type: Number,
required: [true, 'Email validation code is required.'],
},
email_validation_token: {
type: String,
required: [true, 'Email validation token is required.'],
},
// 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: {
pid: {
type: Number,
unique: true
},
username: {
type: String,
unique: true,
validate: [validateUsername, 'Please fill a valid username'],
required: true
},
username_lower: {
type: String,
unique: true
},
avatar_url: {
type: String,
default: '/assets/images/default_avatar.png'
}
},
consoles: []
});
function validateEmail(email) {
// eslint throws "unnecesary character escape"
const re = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/; // eslint-disable-line
return re.test(email);
}
function validateUsername(username) {
// eslint throws "unnecesary character escape"
const re = /^[a-zA-Z0-9]+([_ -]?[a-zA-Z0-9])*$/; // eslint-disable-line
return re.test(username);
}
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: email.toUpperCase()
});
};
PNIDSchema.statics.findUser = async function(key) {
const user = await this.model('pnid').findOne({
email: key.toUpperCase()
});
if (!user) {
return await this.model('pnid').findOne({
'pnid.username_lower': key.toLowerCase()
});
}
return user;
};
PNIDSchema.statics.hashPasswordPrimary = function(password, pid) {
const buff1 = require('python-struct').pack('<I', pid);
const buff2 = Buffer.from(password).toString('ascii');
const unpacked = Buffer.from(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 pid_inuse = await PNIDModel.findOne({
'pnid.pid': pid
});
if (pid_inuse) {
return await PNIDModel.generatePID();
}
return pid;
};
PNIDSchema.statics.generateEmailValidationCode = async function() {
const code = utilHelper.generateRandomInt(6);
const code_inuse = await PNIDModel.findOne({
'email_validation_code': code
});
if (code_inuse) {
return await PNIDModel.generateEmailValidationCode();
}
return code;
};
PNIDSchema.statics.generateEmailValidationToken = async function() {
const token = randtoken.generate(32);
const token_inuse = await PNIDModel.findOne({
'email_validation_token': token
});
if (token_inuse) {
return await PNIDModel.generateEmailValidationToken();
}
return token;
};
const PNIDModel = mongoose.model('pnid', PNIDSchema);
module.exports = {
PNIDModel,
PNIDSchema
};

View File

@@ -1,43 +0,0 @@
/*
post-author.js -
file containing the model for authors
*/
// imports
const mongoose = require('mongoose');
// post author database layout
const postAuthorSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true
},
description: {
type: String,
required: [true, 'Description is required'],
trim: true
},
// profile picture image link
image: {
type: String,
trim: true
}
});
postAuthorSchema.methods.authorTemplate = function() {
return {
name: this.name,
description: this.description,
profilePicture: this.image
};
};
const postAuthorModel = mongoose.model('postAuthor', postAuthorSchema);
module.exports = {
postAuthorModel,
postAuthorSchema
};

View File

@@ -1,36 +0,0 @@
/*
progress-list.js -
file containing the model for progress list
*/
// imports
const mongoose = require('mongoose');
// progress text schema
const progressListSchema = new mongoose.Schema({
state: {
type: Number, /* 0 - nothing, 1 - no support, 2 - partially working, 3 - works */
default: 0
},
isGame: {
type: Boolean, /* true - is game, false - is backend service */
required: [true, 'isGame is required']
},
title: {
type: String,
required: [true, 'Title is required']
},
description: {
type: String,
required: [true, 'Description is required']
}
});
const progressListModel = mongoose.model('progress', progressListSchema);
module.exports = {
progressListModel,
progressListSchema
};

2583
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,25 +15,12 @@
],
"license": "ISC",
"dependencies": {
"bcrypt": "^3.0.1",
"colors": "^1.1.2",
"connect-mongo": "^2.0.1",
"express": "^4.16.2",
"express-handlebars": "^3.0.0",
"express-recaptcha": "^4.0.3",
"express-session": "^1.15.6",
"fs-extra": "^7.0.1",
"moment": "^2.22.2",
"mongoose": "^5.3.2",
"mongoose-unique-validator": "^2.0.2",
"morgan": "^1.9.1",
"multer": "^1.4.1",
"nodemailer": "^4.7.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"python-struct": "^1.0.6",
"rand-token": "^0.4.0",
"showdown": "^1.8.6",
"showdown": "^1.9.1",
"winston": "^3.1.0"
},
"devDependencies": {

View File

@@ -1,100 +0,0 @@
/*
passport.config.js -
Passport setup file. handles authentication strategy and session setup
*/
// imports
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) => {
if (!user) {
// user doesnt exist
return done(null, false, {message: 'Incorrect user'});
}
bcrypt.compare(password, user.password, (err, res) => {
if (err || !res) {
// error comparing hashes
return done(null, false, {message: 'Incorrect password'});
}
// password is correct, return user
return done(null, user);
});
}).catch((err) => {
if (err) {
// error finding in database
return done(null, false, {code: 500});
}
});
}
));
passport.use('PNIDStrategy', new LocalStrategy({
usernameField: 'email'
}, (email, password, done) => {
// find user in database, email = username or email
PNIDModel.findUser(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) {
if (err || !user) {
PNIDModel.findById(id, function(err, user) {
done(err, user);
});
} else {
done(err, user);
}
});
});
};

View File

@@ -1,6 +0,0 @@
User-agent: *
Disallow: /api/
Disallow: /admin
Disallow: /admin/
Disallow: /pnid/dashboard
Disallow: /pnid/dashboard/

View File

@@ -1,494 +0,0 @@
/*
admin.js -
file for handling admin api.
*/
// imports
const router = require('express').Router();
const passport = require('passport');
const moment = require('moment');
const apiHelper = require('../helpers/api');
const userMiddleware = require('../middleware/authentication');
// database models
const adminUser = require('../models/admin-user');
const blogPost = require('../models/blog-post');
const postAuthor = require('../models/post-author');
const progressList = require('../models/progress-list');
// renders admin.hbs
router.get('/admin', (request, response) => {
response.render('admin', {
title: 'Pretendo | Admin',
description: '',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
});
});
/*
* /admin/api/v1/login
*
* signs admin user in
*
* post {
* username
* 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('/admin/api/v1/login', passport.authenticate('adminUserStrategy'), (request, response) => {
const user = request.user;
return apiHelper.sendReturn(response, {
username: user.username,
role: user.role ? user.role : undefined
});
});
/*
* /admin/api/v1/register
* - requires admin auth
*
* registers a new admin user
*
* post {
* username - username of new admin account
* password - password of new admin account
* }
* return {
* code: httpcode
* success: boolean - true if register was successull
* username: undefined | string - username if register was successfull
* role: undefined | string - role of user if register was successfull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/register', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
// no post body
return apiHelper.sendApiGenericError(response);
}
const { username, password } = request.body;
const newUser = new adminUser.adminUserModel({
username,
password
});
// saving to database
newUser.save().then((user) => {
return apiHelper.sendReturn(response, {
username: user.username,
role: user.role ? user.role : undefined
});
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
return apiHelper.sendApiError(response, 500, [rejection]);
});
});
/*
* /admin/api/v1/removeadmin
* - requires admin auth
*
* registers a new admin user
*
* post {
* id - id of the admin user
* }
* return {
* code: httpcode
* success: boolean - true if delete was successull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/removeadmin', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
// no post body
return apiHelper.sendApiGenericError(response);
}
const { id } = request.body;
adminUser.adminUserModel.findByIdAndDelete(id, (error) => {
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
// successfull
return apiHelper.sendReturn(response, {});
});
});
/*
* /admin/api/v1/listadmins
* - requires admin auth
*
* gets list of admins
*
* return {
* code: httpcode
* success: boolean - true if delete was successull
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/listadmins', userMiddleware.adminAuthNeeded, (request, response) => {
adminUser.adminUserModel.find({}, (error, admins) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
// formats admin list and removes password hash
const output = [];
for (let i = 0, l = admins.length; i < l; i++) {
admins[i].password = undefined;
output.push(admins[i]);
}
return apiHelper.sendReturn(response, {
admins: output
});
});
});
/*
* /admin/api/v1/check
*
* checks if admin logged in
*
* return {
* code: httpcode
* success: boolean - true if request was without errors
* isAuthed: boolean - true if logged in
* role: undefined | string - returns user role
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/check', userMiddleware.authOptional, (request, response) => {
const user = request.user;
return apiHelper.sendReturn(response, {
isAuthed: user ? true : false,
role: user ? (user.role ? user.role : undefined) : undefined
});
});
/*
* /admin/api/v1/logout
*
* logs out admin user
*
* return {
* code: httpcode
* success: boolean - true if logout is successfull
* errors: Strings[messages]
* }
*/
router.get('/admin/api/v1/logout', userMiddleware.adminAuthNeeded, (request, response) => {
request.logout();
return apiHelper.sendReturn(response, {});
});
/*
* /admin/api/v1/newpost
*
* posts a new blog post
*
* post {
* content - content of the blog post in markdown
* title - title of the blog post
* author - id of the author
* short - short description of content in plain text
* category - category name of the blog post
*
* }
* return {
* code: http code
* success: boolean - true if login succesfull
* url: string | undefined - url of the blog post if successfull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newpost', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
const { content, title, author, category, short } = request.body;
const newBlogPost = new blogPost.blogPostModel({
content: blogPost.blogPostModel.markdownToHtml(content),
name: title,
short,
meta: {
author,
category,
slug: title // convert title to slug
.trim()
.replace(/\s/g, '-')
.replace(/[^A-z0-9-]/g, '')
.toLowerCase()
}
});
// saving post to database
newBlogPost.save().then((post) => {
return apiHelper.sendReturn(response, {
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
return apiHelper.sendApiError(response, 500, [rejection]);
});
});
/*
* /admin/api/v1/editpost
*
* edits a blog post
*
* post {
* id - id of the blog post to be edited
* content - content of the blog post IN HTML
* title - title of the blog post
* author - id of the author
* short - short description of content in plain text
* category - category name of the blog post
* }
* return {
* code: http code
* success: boolean - true if login succesfull
* url: string | undefined - url of the blog post if successfull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editpost', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
const { id, content, title, author, category, short } = request.body;
blogPost.blogPostModel.findByIdAndUpdate(id, {
'content': content,
'name': title,
'short': short,
'meta.author': author,
'meta.category': category
}, (error, post) => {
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
return apiHelper.sendReturn(response, {
url: moment(post.meta.date, 'YYYY-MM-DD') + '/' + post.meta.slug
});
});
});
/*
* /admin/api/v1/newauthor
*
* creates new author
*
* post {
* name - author name
* description - author description
* image - image url for profile picture
* }
* return {
* code: http code
* success: boolean - true if author succesfull
* id: string | undefined - id of the new author if successfull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newauthor', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
const { name, description, image } = request.body;
const newAuthor = new postAuthor.postAuthorModel({
name,
description,
image
});
// saving author to database
newAuthor.save().then((author) => {
return apiHelper.sendReturn(response, {
id: author.id
});
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
return apiHelper.sendApiError(response, 500, [rejection]);
});
});
/*
* /admin/api/v1/editauthor
*
* edit an existing author
*
* post {
* id - id of author to edit
* name - author name
* description - author description
* image - image url for profile picture
* }
* return {
* code: http code
* success: boolean - true if author succesfull
* id: String - id of the edited author
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editauthor', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
const { id, name, description, image } = request.body;
// updating author in database
postAuthor.postAuthorModel.findByIdAndUpdate(id, {
name,
description,
image
}, (error, author) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
return apiHelper.sendReturn(response, {
id: author.id
});
});
});
/*
* /admin/api/v1/newprogress
*
* creates a new progress entry
*
* post {
* title - progress entry name
* description - progress entry description
* state - 0: backend service entry, 1: no support, 2: partial support, 3: fullly working
* }
* return {
* code: http code
* success: boolean - true if progress succesfull
* id: String | undefined - sends if successfull
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/newprogress', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
// parses state and isGame to be valid
const { title, description } = request.body;
let { state } = request.body;
let isGame = false;
if (state != '1' && state != '2' && state != '3') {
state = undefined;
} else {
state = parseInt(state);
isGame = true;
}
const newProgress = new progressList.progressListModel({
title,
description,
isGame,
state
});
// saving progress to database
newProgress.save().then((progress) => {
return apiHelper.sendReturn(response, {
id: progress.id
});
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
return apiHelper.sendApiError(response, 500, [rejection]);
});
});
/*
* /admin/api/v1/editprogress
*
* edit an existing progress entry
*
* post {
* title - progress entry name
* description - progress entry description
* state - 0: backend service entry, 1: no support, 2: partial support, 3: fullly working
* id - id of entry you want to edit
* }
* return {
* code: http code
* success: boolean - true if progress succesfull
* id: String - id of the edited progress entry
* errors: Strings[messages]
* }
*/
router.post('/admin/api/v1/editprogress', userMiddleware.adminAuthNeeded, (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
// parsing state and isGame to be valid
const { title, description, id } = request.body;
let { state } = request.body;
let isGame = false;
if (state != '1' && state != '2' && state != '3') {
state = undefined;
} else {
state = parseInt(state);
isGame = true;
}
// updating progress in database
progressList.progressListModel.findByIdAndUpdate(id, {
title,
description,
state,
isGame
}, (error, progress) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
return apiHelper.sendReturn(response, {
id: progress.id
});
});
});
// api 404
router.use('/admin/api', (request, response) => {
return apiHelper.sendApi404(response);
});
// export the router
module.exports = router;

View File

@@ -1,134 +0,0 @@
/*
blog.js -
file for handling routes regarding blog posts.
*/
// imports
const logger = require('winston');
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;
const postAuthorModel = require('../models/post-author').postAuthorModel;
// display single blog post
router.get('/news/:date/:title', (request, response) => {
const date = request.params.date;
const title = request.params.title;
const title_lower = title.toLowerCase();
// date format YYYY-MM-DD
if (/[0-9]{4}-[0-9]{2}-[0-9]{2}/.test(date) && /([a-z]|[0-9]|-)+/.test(title_lower)) {
// params are correct format
blogPostModel.getPost(moment(date), title_lower, (error, post) => {
// error exists or no post exists with the date and name
if (error || !post) {
logger.log('warn', `error: ${error} and post: ${post}`);
return utilHelper.send404(response);
}
// render blogpost
post.postTemplate((error, postTemplate) => {
if (error) {
return utilHelper.send404(response);
}
return response.render('post', {
title,
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
post: postTemplate,
user: utilHelper.templateReadyUser(request),
locale: utilHelper.getLocale('US', 'en')
});
});
});
} else {
// params are incorrect
return utilHelper.send404(response);
}
});
// display latest blogposts
router.get('/news', (request, response) => {
// sort blogposts on date descending
blogPostModel.find({}).sort({'meta.date': 'desc'}).exec((error, posts) => {
if (error || !posts) {
return utilHelper.send404(response);
}
// makes posts template ready
const postCollection = [];
for (let i = 0, l = posts.length; i < l; i++) {
postCollection.push(posts[i].postShortTemplate());
}
return response.render('post-collection', {
title: 'Pretendo | News',
description: 'The Pretendo news page has a place for all of the update posts the Pretendo developer team has made. It contains everything you need to know about pretendo.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
posts: postCollection,
user: utilHelper.templateReadyUser(request),
locale: utilHelper.getLocale('US', 'en'),
page: 'news'
});
});
});
/*
* /api/v1/listauthors
*
* gets a list of all authors
*
* return {
* code: http code
* success: boolean - true if author succesfull
* authorList: Objects[{_id, name, description, image}] - list of authors with information
* errors: Strings[messages]
* }
*/
router.get('/api/v1/listauthors', (request, response) => {
postAuthorModel.find({}, (error, authors) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
return apiHelper.sendReturn(response, {
authorList: authors
});
});
});
/*
* /api/v1/listblog
*
* gets a list of all posts
*
* return {
* code: http code
* success: boolean - true if post succesfull
* postList: Objects[{_id, content, meta}] - list of posts with information
* errors: Strings[messages]
* }
*/
router.get('/api/v1/listblog', (request, response) => {
blogPostModel.find({}, (error, posts) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
apiHelper.sendReturn(response, {
postList: posts
});
});
});
// export router
module.exports = router;

16
routes/cemu.js Normal file
View File

@@ -0,0 +1,16 @@
const router = require('express').Router();
const utilHelper = require('../helpers/util');
router.get('/cemu', (request, response) => {
const locale = utilHelper.getLocale('US', 'en');
return response.render('cemu', {
title: 'Pretendo | Cemu',
description: locale.about.text,
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale
});
});
module.exports = router;

16
routes/connecting.js Normal file
View File

@@ -0,0 +1,16 @@
const router = require('express').Router();
const utilHelper = require('../helpers/util');
router.get('/connecting', (request, response) => {
const locale = utilHelper.getLocale('US', 'en');
return response.render('connecting', {
title: 'Pretendo | Connecting',
description: locale.about.text,
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale
});
});
module.exports = router;

View File

@@ -1,91 +0,0 @@
/*
contact.js -
file for handling routes regarding contact
*/
// imports
const logger = require('winston');
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', (request, response) => {
return response.render('contact', {
title: 'Pretendo | Contact',
description: 'The Pretendo Contact page offers a way to ask questions and communicate with the Pretendo developer team. Its the perfect place to get answers.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
user: utilHelper.templateReadyUser(request),
locale: utilHelper.getLocale('US', 'en'),
page: 'contact'
});
});
/*
* /api/v1/sendmessage
*
* registers a new admin user
*
* post {
* email - email of sender
* subject - subject of message
* message - actual message
* }
* return {
* code: httpcode
* success: boolean - true if sending was successull
* errors: Strings[messages]
* }
*/
router.post('/api/v1/sendmessage', (req, response) => {
if (!req.body) {
return apiHelper.sendApiGenericError(response);
}
const { email, subject, message } = req.body;
if (email && subject && message && message.length < 2000) {
// request body has everything
const postData = JSON.stringify({
content: 'email: ' + email + ' \n subject: ' + subject + ' \n\n' + message
});
// request object
const request = https.request({
hostname: config.contactWebhook.host,
port: config.contactWebhook.port,
path: config.contactWebhook.path,
method : 'POST',
headers : {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'Content-Length': postData.length
}
}, () => {
// sends success
return apiHelper.sendReturn(response, {});
});
// error handling
request.on('error', (error) => {
logger.log('warn', 'request errored' + error);
return apiHelper.sendApiGenericError(response);
});
// write post data to request
request.write(postData);
request.end();
} else {
// TODO give more detailed response
return apiHelper.sendApiGenericError(response);
}
});
// export router
module.exports = router;

16
routes/faq.js Normal file
View File

@@ -0,0 +1,16 @@
const router = require('express').Router();
const utilHelper = require('../helpers/util');
router.get('/faq', (request, response) => {
const locale = utilHelper.getLocale('US', 'en');
return response.render('faq', {
title: 'Pretendo | FAQ',
description: locale.about.text,
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale
});
});
module.exports = router;

View File

@@ -9,22 +9,17 @@ 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('/', (request, response) => {
// needs callback because mongoose is inconsistent
blogPostModel.latestPostsShortTemlate(2, (error, result) => {
return response.render('home', {
title: 'Pretendo | Home',
description: 'Pretendo is a as close as possible recreation of the original Nintendo Network for 3ds and Wiiu. It is an replacement for if the original servers shut down.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
user: utilHelper.templateReadyUser(request),
locale: utilHelper.getLocale('US', 'en'),
posts: result,
page: 'home'
});
const locale = utilHelper.getLocale('US', 'en');
return response.render('home', {
title: 'Pretendo | Home',
description: locale.about.text,
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale
});
});

View File

@@ -1,212 +0,0 @@
/*
admin.js -
file for handling admin api.
*/
// imports
const logger = require('winston');
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 mailer = require('../helpers/mailer');
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', (request, response) => {
return response.render('pnid/register', {
title: 'Pretendo | Register',
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale: utilHelper.getLocale('US', 'en'),
recaptcha_sitekey: config.recaptcha.siteKey
});
});
// renders login page
router.get('/pnid/login', (request, response) => {
return response.render('pnid/login', {
title: 'Pretendo | Login',
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale: utilHelper.getLocale('US', 'en')
});
});
// renders password forget
router.get('/pnid/forgotpassword', (request, response) => {
return response.render('pnid/forgotpassword', {
title: 'Pretendo | Forgot password',
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale: utilHelper.getLocale('US', 'en'),
user: utilHelper.templateReadyUser(request)
});
});
// renders password reset page
router.get('/pnid/passwordreset', (request, response) => {
return response.render('passwordreset', {
title: 'Pretendo | Password reset',
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale: utilHelper.getLocale('US', 'en'),
user: utilHelper.templateReadyUser(request)
});
});
// logout
router.get('/pnid/logout', userMiddleware.pnidAuthNeeded, (request, response) => {
request.logout();
return response.redirect('/');
});
// renders pnid dashboard
router.get('/pnid/dashboard', userMiddleware.pnidAuthNeeded, (request, response) => {
return response.render('pnid/dashboard', {
title: 'Pretendo | Dash',
description: 'On the Pretendo Network ID Dashboard you can manage your personal information and make new virtual consoles to use for login on your home console.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale: utilHelper.getLocale('US', 'en'),
user: utilHelper.templateReadyUser(request)
});
});
/*
* /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 (request, response) {
return apiHelper.sendReturn(response, {
email: request.user.email,
email_validated: request.user.email_validated,
pnid: request.user.pnid.key
});
});
/*
* /api/v1/passwordreset
*
* signs user in
*
* post {
* password
* password_confirm
* }
* return {
* code: http code
* success: boolean
* errors: Strings[messages]
* }
*/
router.post('/api/v1/passwordreset', function (request, response) {
//TODO make the actual password change
return apiHelper.sendReturn(response, {});
});
/*
* /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]
* }
*/
const multer = require('multer')();
router.post('/api/v1/register', multer.none(), recaptcha.middleware.verify, async (request, response) => {
if (!request.body) {
return apiHelper.sendApiGenericError(response);
}
console.log(request.body);
return response.send(request.body);
if (request.recaptcha.error) {
logger.log('warn', `[reCaptcha ERROR] ${request.recaptcha.error} | IP: ${request.ip} | Data: ${JSON.stringify(request.body)}`);
return apiHelper.sendApiError(response, 500, ['Captcha error']);
}
const { email, password, confirm_password, username } = request.body;
if (password !== confirm_password) {
return apiHelper.sendApiError(response, 400, ['Passwords do not match']);
}
const email_validation_code = await PNID.PNIDModel.generateEmailValidationCode();
const email_validation_token = await PNID.PNIDModel.generateEmailValidationToken();
const newUser = new PNID.PNIDModel({
email,
email_validation_code,
email_validation_token,
password,
pnid: {
pid: await PNID.PNIDModel.generatePID(),
username,
username_lower: username.toLowerCase()
}
});
// saving to database
newUser.save().then((user) => {
mailer.send(
user.get('email'),
'[Pretendo Network] Please confirm your e-mail address',
`Hello,
Your Pretendo Network ID activation is almost complete. Please click the link below to confirm your e-mail address and complete the activation process.
https://account.pretendo.cc/account/email-confirmation?token${user.get('email_validation_token')}
If you are unable to connect to the above URL, please enter the following confirmation code on the device to which your Pretendo Network ID is linked.
&lt;&lt;Confirmation code: ${user.get('email_validation_code')}&gt;&gt;`
);
return apiHelper.sendReturn(response, {
email: user.email,
email_validated: user.email_validated,
pnid: user.pnid.pid
});
}).catch((rejection) => {
// TODO format exception so it doesnt have a huge list of errors
logger.log('warn', rejection);
return apiHelper.sendApiError(response, 500, [rejection]);
});
});
// export the router
module.exports = router;

View File

@@ -1,66 +0,0 @@
/*
progress.js -
file for handling routes regarding progress
*/
// imports
const router = require('express').Router();
const apiHelper = require('../helpers/api');
const utilHelper = require('../helpers/util');
const progressListModel = require('../models/progress-list').progressListModel;
// display progress
router.get('/progress', (request, response) => {
progressListModel.find({}, (error, progress) => {
if (error) {
return utilHelper.send404(response);
}
// filtering games and backend
const games = progress.filter(i => i.isGame);
const backends = progress.filter(i => !i.isGame);
return response.render('progress', {
title: 'Pretendo | Progress',
description: 'The Pretendo progress page shows the progress the Pretendo developer team made. It shows how much is working and if it can be used for the end user yet.',
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
games,
backends,
user: utilHelper.templateReadyUser(request),
locale: utilHelper.getLocale('US', 'en'),
page: 'progress'
});
});
});
/*
* /api/v1/listprogress
*
* gets a list of progress
*
* return {
* code: http code
* success: boolean - true if progress succesfull
* progressList: Objects[{_id, title, description, state}] - list of progress with information
* errors: Strings[messages]
* }
*/
router.get('/api/v1/listprogress', (request, response) => {
progressListModel.find({}, (error, progress) => {
// TODO format exception so it doesnt have a huge list of errors
if (error) {
return apiHelper.sendApiError(response, 500, [error]);
}
apiHelper.sendReturn(response, {
progressList: progress
});
});
});
// export router
module.exports = router;

View File

@@ -1,33 +1,13 @@
/*
process.on('uncaughtException', console.warn);
server.js -
the file that contains the startup code
*/
// imports
const logger = require('./logger');
const express = require('express');
const handlebars = require('express-handlebars');
const session = require('express-session');
const mongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const morgan = require('morgan');
const logger = require('./logger');
const config = require('./config.json');
const utilHelper = require('./helpers/util');
const passportconfig = require('./passport.config.js');
// setup console colors
require('colors');
// fix database deprecation warnings
mongoose.set('useCreateIndex', true);
mongoose.set('useNewUrlParser', true);
// setup database connection
mongoose.connect(config.database.url);
const connection = mongoose.connection;
connection.on('error', console.error.bind(console, 'connection error:'));
// setup express
const app = express();
app.use(morgan('dev'));
@@ -35,39 +15,12 @@ app.use(express.json());
app.use(express.urlencoded({
extended: true
}));
app.use(session({
secret: config.secrets.session,
saveUninitialized: true,
resave: true,
cookie: {
maxAge: 3600000,
httpOnly: true//, // remove on production
/*secure: true*/ // uncomment on production
},
// permanent session storing for MongoDB
store: new mongoStore({
mongooseConnection: connection,
collection: 'sessions'
})
}));
// setups up passportjs authentication
passportconfig(app);
// handlebars templating setup
const hbsEngine = handlebars({
extname: '.hbs',
layoutsDir: 'views',
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);
}
}
partialsDir: 'views/partials'
});
app.engine('.hbs', hbsEngine);
app.set('view engine', '.hbs');
@@ -75,35 +28,38 @@ app.set('view engine', '.hbs');
// locations and routes setup
const locations = {
home: require('./routes/home'),
posts: require('./routes/blog'),
admin: require('./routes/admin'),
contact: require('./routes/contact'),
progress: require('./routes/progress'),
pnid: require('./routes/pnid')
connecting: require('./routes/connecting'),
cemu: require('./routes/cemu'),
faq: require('./routes/faq'),
};
// static files
app.use('/assets', express.static('assets'));
app.use('/', express.static('public'));
app.use('/assets', express.static(__dirname + '/assets'));
// page map
app.use('/', locations.home);
app.use('/', locations.contact);
app.use('/', locations.posts);
app.use('/', locations.admin);
app.use('/', locations.progress);
app.use('/', locations.pnid);
app.use('/', locations.connecting);
app.use('/', locations.cemu);
app.use('/', locations.faq);
app.use((request, response) => {
return utilHelper.send404(response);
const locale = utilHelper.getLocale('US', 'en');
return response.status(404).render('404', {
title: 'Pretendo | 404',
description: locale.about.text,
url: request.protocol + '://' + request.get('host') + request.originalUrl,
baseurl: request.protocol + '://' + request.get('host'),
locale
});
});
// 4 parameters required to read the error, cant help the eslint error
app.use((error, request, response, next) => { // eslint-disable-line no-unused-vars
logger.log('warn', error.stack);
logger.error(error.stack);
return response.status(500).send('Something broke!');
});
// startup
app.listen(config.http.port, () => {
logger.log('debug', `started the server on port: ${config.http.port}`);
console.log(`started the server on port: ${new String(config.http.port).green}`);
logger.success(`started the server on port: ${config.http.port}`);
});

View File

@@ -1,22 +0,0 @@
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: 'rootroot'
});
newUser.save().then(() => {
console.log('success, user admin pass root');
}).catch((rejection) => {
console.log(rejection);
});

View File

@@ -6,11 +6,13 @@
<link rel="stylesheet" href="/assets/css/pretendo-contact.css">
</head>
<body class="flex">
{{> navbar }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">ERROR</p>
<p class="txt-highlight">404</p>
<h1 class="txt-title">{{ locale.404.title }}</h1>
</div>
</section>

View File

@@ -1,134 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
</head>
<body>
{{> 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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<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>
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

21
views/cemu.hbs Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
</head>
<body>
{{> navbar }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<h1 class="txt-title">Cemu</h1>
<h2>Pretendo is currently not usable by the general public. When the network becomes public, this will contain steps on connecting to the network using Cemu</h2>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

21
views/connecting.hbs Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
</head>
<body>
{{> navbar }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<h1 class="txt-title">Connecting</h1>
<h2>Pretendo is currently not usable by the general public. When the network becomes public, this will contain steps on connecting to the network</h2>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-contact.css">
<!-- scripts -->
<script src="/assets/js/api.js" defer></script>
</head>
<body class="flex">
{{> nav-default }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">{{ locale.contact.caption }}</p>
<h1 class="txt-title">{{ locale.contact.title }}</h1>
<div id="errorBox" class="error hide"></div>
<div id="successBox" class="success hide"></div>
<form action="/api/v1/sendmessage" method="POST">
<div class="input-container">
<input type="text" id="email_input" name="email" placeholder="{{ locale.contact.email_placeholder }}">
<input type="text" id="subject_input" name="subject" placeholder="{{ locale.contact.subject_placeholder }}">
</div>
<textarea name="message" id="message_input" placeholder="{{ locale.contact.message_placeholder }}"></textarea>
</form>
<div class="txt-center">
<button onclick="sendMessage()" class="btn">{{ locale.contact.button_text }}</button>
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

62
views/faq.hbs Normal file
View File

@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-faq.css">
</head>
<body>
{{> navbar }}
<div class="contentWrapper bgLinesSmall">
<div class="bgExtrude"></div>
<section class="bigCardWrapper">
<div class="bigCard">
<h1 class="txt-title">FAQ</h1>
<div class="faqs">
<details>
<summary>What is Pretendo?</summary>
<p class="text">Pretendo is an open source Nintendo Network replacement that aims to build custom servers for the WiiU and 3DS family of consoles. Our goal is to preserve the online functionality of these consoles, to allow players to continue to play their favorite WiiU and 3DS games to their fullest capacity</p>
</details>
<details>
<summary>How do can I use Pretendo?</summary>
<p class="text">Pretendo is currently not in a state that is ready for public use. However, once it is you will be able to use Pretendo simply by running our homebrew patcher on your console</p>
</details>
<details>
<summary>Is there an ETA for XYZ?</summary>
<p class="text">We do not have an exact ETA for any specific features or the project as a whole. There are many parts to the network, updates release for them individually whenever they are ready (for example updates to the account server are (almost always) independent on other parts of the network, such as the friends server). Due to that we cannot give an exact ETA</p>
</details>
<details>
<summary>Does Pretendo work on Cemu/emulators?</summary>
<p class="text">
Pretendo is designed for the WiiU and (eventually) 3DS. At this time the only emulator for these consoles with real online capability is Cemu. Cemu does not officially support custom servers, but it will still be possible to use Pretendo with Cemu
<br>
See the <a href="/cemu">Cemu</a> page for more details
</p>
</details>
<details>
<summary>Will my existing NNIDs work on Pretendo?</summary>
<p class="text">Sadly, no. The only ones with access to the data about your existing NNIDs is Nintendo. A NNID-to-PNID migration could theoretically be possible, but it would be a slow and lengthy process requiring sensitive user information</p>
</details>
<details>
<summary>If I am account/console banned on Nintendo Network, will I stay banned when using Pretendo?</summary>
<p class="text">Any account/console bans only exist server-side. This means any existing bans would not carry over to Pretendo. However Pretendo still reserves the right to issue a ban for our network if we see fit</p>
</details>
<details>
<summary>Will Pretendo support the Wii/Switch?</summary>
<p class="text">The Wii already has custom servers provided by <a href="https://wiimmfi.de/">Wiimmfi</a>. We currently have no plans on supporting the Switch, as not only is it a completely different platform with a different online network, but Switch online is a paid service</p>
</details>
</div>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -6,7 +6,8 @@
<link rel="stylesheet" href="/assets/css/pretendo-home.css">
</head>
<body>
{{> nav-default }}
{{> navbar }}
<div class="contentWrapper">
<header>
<h1>{{ locale.home.header.title }}</h1>
@@ -26,74 +27,74 @@
<section class="bigCardWrapper">
<div class="bigCard">
<p class="txt-highlight">{{ locale.about.caption }}</p>
<p class="txt-highlight">{{ locale.credits.caption }}</p>
<h1 class="txt-title">{{ locale.credits.title }}</h1>
<div class="cardProfiles">
<!-- profile -->
<div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/117261997619412996/c0e34de348b8d45e7a16c0260173bc8c.webp?size=256">
<img class="profileIcon" src="https://avatars3.githubusercontent.com/u/27011796">
<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>
<h2 class="txt-smallHeading">Jonathan Barrow (RedDucks)</h2>
<p>Project owner and lead developer</p>
<a href="https://github.com/RedDuckss" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
</div>
</div>
</section>
<section class="bigCardWrapper">
<div class="bigCard lessPadding cont-flex">
<div class="flex-item">
<p class="txt-highlight">{{ locale.latest_news.caption }}</p>
<h1 class="txt-title">{{ locale.latest_news.title }}</h1>
<p>{{ locale.latest_news.description }}</p>
<a href="/news" class="btn">{{ locale.latest_news.button_text }}</a>
</div>
<div class="cont-flex">
{{#each posts as |post| }}
<div class="smallCardWidth">
<div class="smallCard">
<h3 class="txt-smallHeading">{{ post.title }}</h3>
<p>{{ post.content }}</p>
<a href="/news/{{ post.url }}" class="btn-alt">{{ ../locale.latest_news.news_button_text }}</a>
<div class="profile">
<img class="profileIcon" src="https://avatars3.githubusercontent.com/u/32065563">
<div class="profileText">
<h2 class="txt-smallHeading">Jemma (CaramelKat)</h2>
<p>Miiverse research and development</p>
<a href="https://github.com/CaramelKat" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://avatars3.githubusercontent.com/u/10212424">
<div class="profileText">
<h2 class="txt-smallHeading">superwhiskers</h2>
<p>crunch library development</p>
<a href="https://github.com/superwhiskers" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://avatars1.githubusercontent.com/u/19657053">
<div class="profileText">
<h2 class="txt-smallHeading">SuperMarioDaBom</h2>
<p>Console and other system research</p>
<a href="https://github.com/SuperMarioDaBom" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/326811693751468033/1064818534277b6cafbb4b53a9a36d3e.png?size=2048">
<div class="profileText">
<h2 class="txt-smallHeading">Rambo6Glaz</h2>
<p>Network installer and console research</p>
<a href="https://github.com/NexoDevelopment" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://avatars2.githubusercontent.com/u/19330965">
<div class="profileText">
<h2 class="txt-smallHeading">mrjvs</h2>
<p>Web development and early eShop research</p>
<a href="https://github.com/mrjvs" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://avatars1.githubusercontent.com/u/47010303">
<div class="profileText">
<h2 class="txt-smallHeading">Benjamin Moss (kxpler442b)</h2>
<p>Web development</p>
<a href="https://github.com/kxpler442b" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
<div class="profile">
<img class="profileIcon" src="https://cdn.discordapp.com/avatars/186572995848830987/b55c0d4e7bfd792edf0689f83a25d8ea.png?size=2048">
<div class="profileText">
<h2 class="txt-smallHeading">Kinnay</h2>
<p>None of this would be possible without the research and help done by Kinnay on PRUDP and nex</p>
<a href="https://github.com/Kinnay" target="_blank"><button class="btn small">GitHub</button></a>
</div>
</div>
{{/each }}
</div>
</div>
</section>

View File

@@ -1,10 +0,0 @@
<nav class="userNav">
<div class="userNavProfile">
<img src="{{ user.user.pnid.avatar_url }}" alt="{{ user.user.pnid.username }}">
<p class="name">{{ user.user.pnid.username }}</p>
</div>
<div class="links">
<a class="navItem active" href="#">{{ locale.dashboard.navigation.links.account }}</a>
<a class="navItem" href="#">{{ locale.dashboard.navigation.links.soon }}</a>
</div>
</nav>

View File

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

14
views/partials/navbar.hbs Normal file
View File

@@ -0,0 +1,14 @@
<nav class="navWrapper" id="nav">
<a class="navItem brand" href="/">{{ locale.nav.brand }}</a>
<a class="navItem" href="/connecting">Connecting</a>
<a class="navItem" href="/cemu">Cemu</a>
<a class="navItem" href="/faq">FAQ</a>
<a class="navItem" href="https://github.com/PretendoNetwork/" target="_blank">GitHub</a>
<a class="navItem" href="https://invite.gg/pretendo" target="_blank">Discord</a>
<div class="navSpread"></div>
<a class="navItem hamburger" id="navToggle">
<span></span>
<span></span>
<span></span>
</a>
</nav>

View File

@@ -1,38 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> head-common }}
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-pnid.css">
<!-- scripts -->
<script src="/assets/js/api.js"></script>
<script src="/assets/js/pnid.js"></script>
</head>
<body class="flex">
{{> nav-default }}
<div id="email_verification_popup" class="email_popup{{#if_eq user.user.email_validated true}} hide{{/if_eq}}"><p>{{ locale.verify_email_popup.text }}</p><span><button class="btn-alt small" onclick="toggleEditInfo();">{{ locale.verify_email_popup.change_button_text }}</button> <button class="btn small" onclick="email_resend();">{{ locale.verify_email_popup.repeat_button_text }}</button></span></div>
<div class="contentWrapper userPage">
{{> nav-dashboard }}
<section class="contentCard">
<p class="txt-highlight">{{ locale.dashboard.account.caption }}</p>
<h1 class="txt-title small">{{ locale.dashboard.account.title }}</h1>
<div class="infoContainer" id="userInfo">
<p class="infoList"><span>Username:</span> {{ user.user.pnid.username }}</p>
<p class="infoList"><span>Email:</span> {{ user.user.email }}</p>
<button class="btn small btn-alt" onclick="toggleEditInfo();">Edit</button>
</div>
<div class="infoContainer hide" id="editInfo">
<div id="errorBox" class="error hide"></div>
<p class="infoList"><span>Username:</span>
<input type="text" class="inline" name="username_input" id="username_input" value="{{ user.user.pnid.username }}">
</p>
<p class="infoList"><span>Email:</span>
<input type="text" class="inline" name="email_input" id="email_input" value="{{ user.user.email }}">
</p>
<button class="btn small" onclick="editInfo();">Save</button><a href="#" class="link-margin" onclick="toggleEditInfo();">Cancel</a>
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> metatags }}
<title>{{ title }}</title>
<!-- css files -->
<link rel="stylesheet" href="/assets/css/pretendo-common.css">
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src="/assets/js/api.js" defer></script>
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locale.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">{{ locale.forgot.card.caption }}</p>
<h1 class="txt-title small">{{ locale.forgot.card.title }}</h1>
<div id="errorBox" class="error hide"></div>
<form method="POST">
<label for="email_input">{{ locale.forgot.card.email_text}}</label>
<input type="text" class="hasLabel" id="email_input" name="email_input">
</form>
<button class="btn btnMargin" onclick="passwordForgot();">{{ locale.forgot.card.button_text }}</button>
</div>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,32 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> metatags }}
<title>{{ title }}</title>
<!-- css files -->
<link rel="stylesheet" href="/assets/css/pretendo-common.css">
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src="/assets/js/api.js" defer></script>
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locale.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">{{ locale.login.card.caption }}</p>
<h1 class="txt-title small">{{ locale.login.card.title }}</h1>
<div id="errorBox" class="error hide"></div>
<form method="POST">
<label for="email">{{ locale.login.card.email_text }}</label>
<input type="text" class="hasLabel" id="email_input" name="email">
<label for="email">{{ locale.login.card.password_text }}</label>
<input type="password" class="hasLabel" id="password_input" name="password">
<p class="lowText">{{ locale.login.card.forgotpass_introduction }} <a href="/pnid/forgotpassword">{{ locale.login.card.forgotpass_text }}</a></p>
</form>
<button class="btn btnMargin" onclick="login();">{{ locale.login.card.button_text }}</button>
<p class="lowText">{{ locale.login.card.link_introduction }} <a href="/pnid/register">{{ locale.login.card.link_text }}</a></p>
</div>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> metatags }}
<title>{{ title }}</title>
<!-- css files -->
<link rel="stylesheet" href="/assets/css/pretendo-common.css">
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src="/assets/js/api.js" defer></script>
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locale.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">{{ locale.password.card.caption }}</p>
<h1 class="txt-title small">{{ locale.password.card.title }}</h1>
<div id="errorBox" class="error hide"></div>
<form method="POST">
<label for="password_input">{{ locale.password.card.password_text}}</label>
<input type="password" class="hasLabel" id="password_input" name="password_input">
<label for="password_confirm">{{ locale.password.card.password_confirm_text }}</label>
<input type="password" class="hasLabel" id="password_confirm" name="password_confirm">
</form>
<button class="btn btnMargin" onclick="passwordReset();">{{ locale.password.card.button_text }}</button>
</div>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,147 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> metatags }}
<title>{{ title }}</title>
<!-- css files -->
<link rel="stylesheet" href="/assets/css/pretendo-common.css">
<!-- page specific -->
<link rel="stylesheet" href="/assets/css/pretendo-auth.css">
<script src='https://www.google.com/recaptcha/api.js'></script>
<script src="/assets/js/api.js" defer></script>
<!-- date picker -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.css">
</head>
<body class="flex center">
<div class="card">
<h2 class="head">{{ locale.nav.brand }}</h2>
<div class="content">
<p class="txt-highlight">{{ locale.register.card.caption }}</p>
<h1 class="txt-title small">{{ locale.register.card.title }}</h1>
<div id="errorBox" class="error hide"></div>
<form id="registerForm" method="POST" action="http://account.pretendo.cc/v1/api/people/registerwp">
<label for="birthdate">{{ locale.register.card.birthdate_text }}</label>
<input required type="text" class="hasLabel" id="birthdate_input" name="birthdate">
<label for="gender">{{ locale.register.card.gender_text }}</label>
<input required type="text" class="hasLabel" id="gender_input" name="gender" list="gender-list" autocomplete="new-password">
<datalist id="gender-list">
<option value="M"></option>
<option value="F"></option>
</datalist>
<label for="email">{{ locale.register.card.email_text }}</label>
<input required type="text" class="hasLabel" id="email_input" name="email">
<label for="username">{{ locale.register.card.username_text }}</label>
<input required type="text" class="hasLabel" id="username_input" name="username" minlength="6" maxlength="16">
<label for="password">{{ locale.register.card.password_text }}</label>
<input required type="password" class="hasLabel" id="password_input" name="password">
<label for="confirm_password">{{ locale.register.card.password_confirm_text }}</label>
<input required type="password" class="hasLabel" id="confirm_password_input" name="confirm_password">
<div class="g-recaptcha"
data-callback="capchaFilled"
data-expired-callback="capchaExpired"
data-sitekey="{{ recaptcha_sitekey }}"></div>
<input disabled type="submit" value="{{ locale.register.card.button_text }}" class="btn btnMargin"></input>
</form>
<p class="lowText">{{ locale.register.card.link_introduction }} <a href="/pnid/login">{{ locale.register.card.link_text }}</a></p>
</div>
</div>
<script>
$('#birthdate_input').datepicker({
autoPick: true,
format: 'yyyy-mm-dd',
startView: 2
});
const form = document.getElementById('registerForm');
form.addEventListener('submit', async e => {
e.preventDefault();
// https://stackoverflow.com/a/34296482
const elements = [].filter.call(form.elements, element => {
return !(element.type in ['checkbox', 'radio']) || element.checked;
})
.filter(element => { return !!element.name })
.filter(element => { return !element.disabled });
const data = {};
for ({name, value} of elements) {
data[name] = value;
}
if (!data['g-recaptcha-response'] || data['g-recaptcha-response'].trim() === '') {
alert('Fill in the captcha');
return false;
}
if (
!data.email ||
!data.username ||
!data.password ||
!data.confirm_password ||
data.email.trim() === '' ||
data.username.trim() === '' ||
data.password.trim() === '' ||
data.confirm_password.trim() === ''
) {
// Handle error, there are missing fields
console.log(data);
alert('error')
return false;
}
if (data.password.trim() !== data.confirm_password.trim()) {
alert('Passwords do not match');
return false;
}
const response = await fetch(`http://account.pretendo.cc/v1/api/people/${data.username}`, {
headers: {
'x-nintendo-client-id': 'a2efa818a34fa16b8afbc8a74eba3eda',
'x-nintendo-client-secret': 'c91cdb5658bd4954ade78533a339cf9a'
}
});
const text = await response.text();
if (text.trim() !== '') {
const body = (new window.DOMParser()).parseFromString(text, 'text/xml');
const error = body;
alert('Username already in use');
return; // Handle errors here
}
const params = elements.map(({name, value}) => {
return encodeURIComponent(name) + '=' + encodeURIComponent(value);
}).join('&');
console.log(params);
postAjax(form.action, params, {
load: event => {
console.log(event.target.responseText)
},
error: event => {
console.log(event)
}
});
});
function capchaFilled() {
document.querySelector('input[type="submit"]').removeAttribute('disabled');
}
function capchaExpired() {
document.querySelector('input[type="submit"]').setAttribute('disabled', true);
}
</script>
{{> footer-default }}
</body>
</html>

View File

@@ -1,29 +0,0 @@
<!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">{{ locale.news.caption }}</p>
<h1 class="txt-title">{{ locale.news.title }}</h1><!--
{{#each posts as |post| }}
--><article class="smallCard">
<h3 class="txt-smallHeading">{{ post.title }}</h3>
<p>{{ post.content }}</p>
<a class="btn-alt" href="/news/{{ post.url }}">{{ ../locale.news.button_text }}</a>
</article><!--
{{/each}}
-->
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>

View File

@@ -1,28 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{> 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.dateString }}</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,44 +0,0 @@
<!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">{{ locale.progress.summary.caption }}</p>
<h1 class="txt-title">{{ locale.progress.summary.title }}</h1>
<p>{{ locale.progress.summary.text }}</p>
</div>
</section>
<section class="bigCardWrapper double">
<div class="bigCard">
<p class="txt-highlight">{{ locale.progress.compatibility.caption }}</p>
<h1 class="txt-title">{{ locale.progress.compatibility.title }}</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">{{ locale.progress.misc.caption }}</p>
<h1 class="txt-title">{{ locale.progress.misc.title }}</h1>
{{#each backends }}
<div class="listItem">
<h2>{{ this.title }}</h2>
<p>{{ this.description }}</p>
</div>
{{/each}}
</div>
</section>
</div>
{{> footer-default }}
</body>
</html>