This commit is contained in:
Ash Monty 2021-12-13 16:24:41 +01:00
commit b9f4da3f55
No known key found for this signature in database
GPG Key ID: 24F2A3D0CBE0A68F
13 changed files with 2841 additions and 45 deletions

View File

@ -16,5 +16,6 @@
"tester_roles": [
"role id"
]
}
},
"aes_key": "hex key here"
}

2474
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
},
"homepage": "https://github.com/PretendoNetwork/website#readme",
"dependencies": {
"adm-zip": "^0.5.9",
"colors": "^1.4.0",
"cookie-parser": "^1.4.5",
"discord-oauth2": "github:ryanblenis/discord-oauth2",
@ -28,7 +29,8 @@
"gray-matter": "^4.0.3",
"marked": "^3.0.4",
"morgan": "^1.10.0",
"trello": "^0.11.0"
"trello": "^0.11.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"eslint": "^7.32.0"

View File

@ -25,6 +25,17 @@
border-radius: 100%;
background: var(--btn-secondary);
}
.account-sidebar .user #download-cemu-files {
width: 100%;
padding: 4px 16px;
cursor: pointer;
background: #383f6b;
font-size: 0.9rem;
margin: 8px 0 0;
}
.account-sidebar .user .cemu-warning {
font-size: 0.7rem;
}
/* Settings */
.settings-wrapper {

View File

@ -0,0 +1,95 @@
.wrapper {
display: flex;
flex-flow: column;
min-height: 100vh;
}
.register-wrapper {
margin: auto;
}
form.register {
padding: 48px;
background-color: #151934;
border-radius: 12px;
max-width: calc(90vw - 92px);
width: 400px;
}
form.register input {
appearance: none;
-webkit-appearance: none;
display: block;
border: 0;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
background-color: var(--btn-secondary);
border: none;
border-radius: 4px;
padding: 12px;
color: var(--text);
width: calc(100% - 24px);
margin-bottom: 12px;
}
form.register input::placeholder {
color: var(--text-secondary);
}
form.register input:focus {
background-color: #fff;
color: var(--btn-secondary);
transition: 200ms;
outline: none;
}
form.register button {
appearance: none;
-webkit-appearance: none;
display: block;
border: 0;
border-radius: 4px;
font-family: Poppins, Arial, Helvetica, sans-serif;
font-size: 1rem;
color: var(--text);
padding: 12px;
background: var(--btn);
cursor: pointer;
width: 100%;
}
form.register .login {
text-decoration: none;
}
form.register .login button {
background: var(--btn-secondary);
margin-top: 18px;
}
footer {
margin-top: 23px;
}
@keyframes banner-notice {
0% {top: -150px}
20% {top: 35px}
80% {top: 35px}
100% {top: -150px}
}
.banner-notice {
display: flex;
justify-content: center;
position: fixed;
top: -150px;
width: 100%;
animation: banner-notice 5s;
}
.banner-notice div {
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
}
.banner-notice.error div {
background: #A9375B;
}

View File

@ -1,5 +1,5 @@
document.getElementById('remove-discord-connection').addEventListener('click', () => {
document.getElementById('remove-discord-connection')?.addEventListener('click', () => {
// TODO: Refresh access token if expired (move this to the backend maybe?)
const tokenType = document.cookie.split('; ').find(row => row.startsWith('token_type=')).split('=')[1];

View File

@ -1,9 +1,12 @@
const { Router } = require('express');
const crypto = require('crypto');
const DiscordOauth2 = require('discord-oauth2');
const { v4: uuidv4 } = require('uuid');
const AdmZip = require('adm-zip');
const util = require('../util');
const config = require('../../config.json');
const router = new Router();
const aesKey = Buffer.from(config.aes_key, 'hex');
// Create OAuth client
const discordOAuth = new DiscordOauth2({
@ -15,7 +18,7 @@ const discordOAuth = new DiscordOauth2({
router.get('/', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token) {
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
return response.redirect('/account/login');
}
@ -29,8 +32,8 @@ router.get('/', async (request, response) => {
};
// Reset message cookies
response.clearCookie('linked');
response.clearCookie('error');
response.clearCookie('linked', { domain: '.pretendo.network' });
response.clearCookie('error', { domain: '.pretendo.network' });
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
@ -183,7 +186,7 @@ router.get('/login', async (request, response) => {
error: request.cookies.error
};
response.clearCookie('error');
response.clearCookie('error', { domain: '.pretendo.network' });
response.render('account_login', renderData);
});
@ -191,7 +194,7 @@ router.get('/login', async (request, response) => {
router.post('/login', async (request, response) => {
const { username, password } = request.body;
const apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
let apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
username,
password,
grant_type: 'password'
@ -208,6 +211,71 @@ router.post('/login', async (request, response) => {
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
const account = apiResponse.body;
const hashedPassword = util.nintendoPasswordHash(password, account.pid);
const hashedPasswordBuffer = Buffer.from(hashedPassword, 'hex');
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let encryptedBody = cipher.update(hashedPasswordBuffer);
encryptedBody = Buffer.concat([encryptedBody, cipher.final()]);
response.cookie('ph', encryptedBody.toString('hex'), { domain: '.pretendo.network' });
response.redirect('/account');
});
router.get('/register', async (request, response) => {
const renderData = {
layout: 'main',
locale: util.getLocale(request.locale.region, request.locale.language),
localeString: request.locale.toString(),
error: request.cookies.error,
email: request.cookies.email,
username: request.cookies.username,
mii_name: request.cookies.mii_name,
};
response.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.render('account_register', renderData);
});
router.post('/register', async (request, response) => {
const { email, username, mii_name, password, password_confirm } = request.body;
response.cookie('email', email, { domain: '.pretendo.network' });
response.cookie('username', username, { domain: '.pretendo.network' });
response.cookie('mii_name', mii_name, { domain: '.pretendo.network' });
const apiResponse = await util.apiPostGetRequest('/v1/register', {}, {
email, username, mii_name, password, password_confirm
});
if (apiResponse.statusCode !== 200) {
response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' });
return response.redirect('/account/register');
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
response.clearCookie('error', { domain: '.pretendo.network' });
response.clearCookie('email', { domain: '.pretendo.network' });
response.clearCookie('username', { domain: '.pretendo.network' });
response.clearCookie('mii_name', { domain: '.pretendo.network' });
response.redirect('/account');
});
@ -255,9 +323,9 @@ router.get('/connect/discord', async (request, response) => {
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain : '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain : '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain : '.pretendo.network' });
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
@ -281,4 +349,91 @@ router.get('/connect/discord', async (request, response) => {
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
});
router.get('/online-files', async (request, response) => {
// Verify the user is logged in
if (!request.cookies.access_token || !request.cookies.refresh_token|| !request.cookies.ph) {
return response.redirect('/account/login');
}
// Attempt to get user data
let apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
});
if (apiResponse.statusCode !== 200) {
// Assume expired, refresh and retry request
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
refresh_token: request.cookies.refresh_token,
grant_type: 'refresh_token'
});
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
const tokens = apiResponse.body;
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
apiResponse = await util.apiGetRequest('/v1/user', {
'Authorization': `${tokens.token_type} ${tokens.access_token}`
});
}
// If still failed, something went horribly wrong
if (apiResponse.statusCode !== 200) {
// TODO: Error message
return response.status(apiResponse.statusCode).json({
error: 'Bad'
});
}
const account = apiResponse.body;
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, Buffer.alloc(16));
let decryptedPasswordHash = decipher.update(Buffer.from(request.cookies.ph, 'hex'));
decryptedPasswordHash = Buffer.concat([decryptedPasswordHash, decipher.final()]);
const miiNameBuffer = Buffer.alloc(0x16);
const miiName = Buffer.from(account.mii.name, 'utf16le').swap16();
miiName.copy(miiNameBuffer);
let accountDat = 'AccountInstance_00000000\n';
accountDat += 'PersistentId=80000001\n';
accountDat += 'TransferableIdBase=0\n';
accountDat += `Uuid=${uuidv4().replace(/-/g, '')}\n`;
accountDat += `MiiData=${Buffer.from(account.mii.data, 'base64').toString('hex')}\n`;
accountDat += `MiiName=${miiNameBuffer.toString('hex')}\n`;
accountDat += `AccountId=${account.username}\n`;
accountDat += 'BirthYear=0\n';
accountDat += 'BirthMonth=0\n';
accountDat += 'BirthDay=0\n';
accountDat += 'Gender=0\n';
accountDat += `EmailAddress=${account.email.address}\n`;
accountDat += 'Country=0\n';
accountDat += 'SimpleAddressId=0\n';
accountDat += `PrincipalId=${account.pid.toString(16)}\n`;
accountDat += 'IsPasswordCacheEnabled=1\n';
accountDat += `AccountPasswordCache=${decryptedPasswordHash.toString('hex')}`;
const onlineFiles = new AdmZip();
onlineFiles.addFile('mlc01/usr/save/system/act/80000001/account.dat', Buffer.from(accountDat)); // Minimal account.dat
onlineFiles.addFile('otp.bin', Buffer.alloc(0x400)); // nulled OTP
onlineFiles.addFile('seeprom.bin', Buffer.alloc(0x200)); // nulled SEEPROM
response.status(200);
response.set('Content-Disposition', 'attachment; filename="Online Files.zip');
response.set('Content-Type', 'application/zip');
response.end(onlineFiles.toBuffer());
});
module.exports = router;

View File

@ -1,5 +1,6 @@
const fs = require('fs-extra');
const got = require('got');
const crypto = require('crypto');
const logger = require('./logger');
function fullUrl(request) {
@ -52,10 +53,26 @@ function apiDeleteGetRequest(path, headers, json) {
});
}
function nintendoPasswordHash(password, pid) {
const pidBuffer = Buffer.alloc(4);
pidBuffer.writeUInt32LE(pid);
const unpacked = Buffer.concat([
pidBuffer,
Buffer.from('\x02\x65\x43\x46'),
Buffer.from(password)
]);
const hashed = crypto.createHash('sha256').update(unpacked).digest().toString('hex');
return hashed;
}
module.exports = {
fullUrl,
getLocale,
apiGetRequest,
apiPostGetRequest,
apiDeleteGetRequest
apiDeleteGetRequest,
nintendoPasswordHash
};

View File

@ -9,7 +9,9 @@
<div class="user">
<img src="{{account.mii.image_url}}" class="mii" />
<p class="miiname">{{account.mii.name}}</p>
<p class="username">PNID: {{account.username}}</p>
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>Download account files</a>
<p class="cemu-warning">(will not work with Nintendo Network)</p>
</div>
</div>

View File

@ -0,0 +1,47 @@
<link rel="stylesheet" href="/assets/css/register.css" />
<div class="wrapper">
{{> header}}
<div class="register-wrapper">
<h1 class="title dot">Register</h1>
<form action="/account/register" method="post" class="register">
<div>
<input name="email" id="email" type="email" placeholder="Email" value="{{ email }}" required>
</div>
<div>
<input name="username" id="username" placeholder="Username" value="{{ username }}" minlength=6 maxlength=16 required>
</div>
<div>
<input name="mii_name" id="mii_name" placeholder="Mii Name (Max 10 characters)" value="{{ mii_name }}" maxlength=10 required>
</div>
<div>
<input name="password" id="password" type="password" placeholder="Password" autocomplete="new-password" required>
</div>
<div>
<input name="password_confirm" id="password_confirm" type="password" placeholder="Confirm Password" required>
</div>
<div>
<button type="submit">Register</button>
</div>
<div>
<a href="/account/login" class="login">
<button type="button">Already have an account?</button>
</a>
</div>
</form>
</div>
{{> footer }}
</div>
{{#if error}}
<div class="banner-notice error">
<div>
<p>{{ error }}</p>
</div>
</div>
{{/if}}

View File

@ -116,18 +116,18 @@
<div class="grid">
<div class="item highlight">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
<h1>Juxtaposition</h1>
<p>A Miiverse replacement</p>
<h1>Game Servers</h1>
<p>Bringing back your favorite games and content using custom servers</p>
</div>
<div class="item">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
<h1>Juxtaposition</h1>
<p>A Miiverse replacement</p>
<p>A re-imagining of Miiverse, as if it were made in the modern era</p>
</div>
<div class="item">
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="book" class="svg-inline--fa fa-book fa-w-14" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M448 360V24c0-13.3-10.7-24-24-24H96C43 0 0 43 0 96v320c0 53 43 96 96 96h328c13.3 0 24-10.7 24-24v-16c0-7.5-3.5-14.3-8.9-18.7-4.2-15.4-4.2-59.3 0-74.7 5.4-4.3 8.9-11.1 8.9-18.6zM128 134c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm0 64c0-3.3 2.7-6 6-6h212c3.3 0 6 2.7 6 6v20c0 3.3-2.7 6-6 6H134c-3.3 0-6-2.7-6-6v-20zm253.4 250H96c-17.7 0-32-14.3-32-32 0-17.6 14.4-32 32-32h285.4c-1.9 17.1-1.9 46.9 0 64z"></path></svg>
<h1>Juxtaposition</h1>
<p>A Miiverse replacement</p>
<h1>Cemu Support</h1>
<p>Play your favorite Wii U titles even without a console!</p>
</div>
</div>
</section>

View File

@ -1,7 +1,6 @@
<footer>
<div>
<svg class="logotype" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 39.876" preserveAspectRatio="xMinYMin meet"
>
<div>
<svg class="logotype" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 39.876" preserveAspectRatio="xMinYMin meet">
<g id="logo_type" data-name="logo type" transform="translate(-553 -467)">
<g id="logo" transform="translate(553 467)">
<rect id="XMLID_158_" width="39.876" height="39.876" fill="#9d6ff3" opacity="0" />
@ -23,26 +22,25 @@
</text>
</g>
</svg>
<p>Copyright 2021</p>
<p>Site by <a href="https://mrjvs.com/" target="_blank">mrjvs</a>, <a href="https://jipfr.nl/" target="_blank">jipfr</a> & <a href="https://ashmonty.com/" target="_blank">monty</a></p>
</div>
<div>
<h1>Socials</h1>
<a href="https://twitter.com/PretendoNetwork/" target="_blank">Twitter</a>
<a href="https://patreon.com/PretendoNetwork" target="_blank">Patreon</a>
<a href="https://ko-fi.com/PretendoNetwork" target="_blank">Ko-fi</a>
<a href="https://invite.gg/pretendo" target="_blank">Discord</a>
<a href="https://github.com/PretendoNetwork" target="_blank">GitHub</a>
</div>
<div>
<h1>Useful links</h1>
<a href="/docs" style="display: none;">Docs</a>
<p>Copyright 2021</p>
<p>Site by <a href="https://mrjvs.com/" target="_blank">mrjvs</a>, <a href="https://jipfr.nl/" target="_blank">jipfr</a> & <a href="https://ashmonty.com/" target="_blank">monty</a></p>
</div>
<div>
<h1>Socials</h1>
<a href="https://twitter.com/PretendoNetwork/" target="_blank">Twitter</a>
<a href="https://patreon.com/PretendoNetwork" target="_blank">Patreon</a>
<a href="https://invite.gg/pretendo" target="_blank">Discord</a>
<a href="https://github.com/PretendoNetwork" target="_blank">GitHub</a>
</div>
<div>
<h1>Useful links</h1>
<a style="display: none;" href="/docs">Docs</a>
<a href="/faq">FAQ</a>
<a href="/progress">Progress</a>
<a href="/blog">Blog</a>
<a href="/account" style="display: none; /* Hiding for now since it's not ready */">Account</a>
</div>
<div class="discord-server-card">
<a href="/progress">Progress</a>
<a href="/blog">Blog</a>
<a href="/account" style="display: none; /* Hiding for now since it's not ready */">Account</a>
</div>
<div class="discord-server-card">
<h1>Want to keep updated?</h1>
<h2>Join our Discord server!</h2>
<a href="https://invite.gg/pretendo">
@ -51,6 +49,5 @@
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
Join now!</a>
</div>
</footer>

View File

@ -28,7 +28,7 @@
<a href="/#about">{{ localeHelper locale "nav" "about" }}</a>
<a href="/#faq">{{ localeHelper locale "nav" "faq" }}</a>
<a href="/#credits">{{ localeHelper locale "nav" "credits" }}</a>
<a href="/docs" style="display: none;">{{ localeHelper locale "nav" "docs"}}</a>
<a style="display: none;" href="/docs">{{ localeHelper locale "nav" "docs"}}</a>
<a href="/progress" class="keep-on-mobile">{{ localeHelper locale "nav" "progress"}}</a>
<a href="/blog" class="keep-on-mobile">{{ localeHelper locale "nav" "blog" }}</a>
</nav>
@ -118,5 +118,4 @@
<div class="item"><span class="lang"></span></div>
</div>
</div>
</header>