diff --git a/example.config.json b/example.config.json index 4987193..7afb450 100644 --- a/example.config.json +++ b/example.config.json @@ -11,11 +11,7 @@ "discord": { "client_id": "client_id", "client_secret": "client_secret", - "guild_id": "Guild ID", - "bot_token": "token", - "tester_roles": [ - "role id" - ] + "bot_token": "token" }, "stripe": { "secret_key": "sk_secret", @@ -34,6 +30,5 @@ "gmail": { "user": "email@gmail.com", "pass": "app-password" - }, - "aes_key": "hex key here" + } } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 681e34c..cc64560 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@discordjs/rest": "^0.5.0", "adm-zip": "^0.5.9", "browserify": "^17.0.0", "colors": "^1.4.0", "cookie-parser": "^1.4.5", + "discord-api-types": "^0.36.1", "discord-oauth2": "github:ryanblenis/discord-oauth2", "express": "^4.17.1", "express-handlebars": "^5.3.1", @@ -136,6 +138,35 @@ "node": ">=4" } }, + "node_modules/@discordjs/collection": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.7.0.tgz", + "integrity": "sha512-R5i8Wb8kIcBAFEPLLf7LVBQKBDYUL+ekb23sOgpkpyGT+V4P7V83wTxcsqmX+PbqHt4cEHn053uMWfRqh/Z/nA==", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.5.0.tgz", + "integrity": "sha512-S4E1YNz1UxgUfMPpMeqzPPkCfXE877zOsvKM5WEmwIhcpz1PQV7lzqlEOuz194UuwOJLLjQFBgQELnQfCX9UfA==", + "dependencies": { + "@discordjs/collection": "^0.7.0", + "@sapphire/async-queue": "^1.3.1", + "@sapphire/snowflake": "^3.2.2", + "discord-api-types": "^0.33.3", + "tslib": "^2.4.0", + "undici": "^5.4.0" + }, + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/@discordjs/rest/node_modules/discord-api-types": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.33.5.tgz", + "integrity": "sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==" + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -222,6 +253,24 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, + "node_modules/@sapphire/async-queue": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", + "integrity": "sha512-FFTlPOWZX1kDj9xCAsRzH5xEJfawg1lNoYAA+ecOWJMHOfiZYb1uXOI3ne9U4UILSEPwfE68p3T9wUHwIQfR0g==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.2.tgz", + "integrity": "sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", @@ -1255,6 +1304,11 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, + "node_modules/discord-api-types": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.1.tgz", + "integrity": "sha512-PTDinUU574hXA9Ko9wrftL1iww1raNiRVKjuPIWQ5Li1g7vQPArpZWw9x01kh/IXLPdzSAJ6H8T0eAYzxzFzIg==" + }, "node_modules/discord-oauth2": { "version": "2.9.0", "resolved": "git+ssh://git@github.com/ryanblenis/discord-oauth2.git#ca0093d5b34efdd6ec2ce45a4a2678b3282cea31", @@ -4116,6 +4170,11 @@ "node": ">= 0.10.x" } }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", @@ -4214,6 +4273,14 @@ "undeclared-identifiers": "bin.js" } }, + "node_modules/undici": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.6.1.tgz", + "integrity": "sha512-yYVqywdCbNb99f/w045wqmv++WExXDjY0FEvLSB7QUZZH6izxrVkF4dJn1aimcvN0+WAhv75Gg7v6VJoqmRtJQ==", + "engines": { + "node": ">=12.18" + } + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -4519,6 +4586,31 @@ } } }, + "@discordjs/collection": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.7.0.tgz", + "integrity": "sha512-R5i8Wb8kIcBAFEPLLf7LVBQKBDYUL+ekb23sOgpkpyGT+V4P7V83wTxcsqmX+PbqHt4cEHn053uMWfRqh/Z/nA==" + }, + "@discordjs/rest": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-0.5.0.tgz", + "integrity": "sha512-S4E1YNz1UxgUfMPpMeqzPPkCfXE877zOsvKM5WEmwIhcpz1PQV7lzqlEOuz194UuwOJLLjQFBgQELnQfCX9UfA==", + "requires": { + "@discordjs/collection": "^0.7.0", + "@sapphire/async-queue": "^1.3.1", + "@sapphire/snowflake": "^3.2.2", + "discord-api-types": "^0.33.3", + "tslib": "^2.4.0", + "undici": "^5.4.0" + }, + "dependencies": { + "discord-api-types": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.33.5.tgz", + "integrity": "sha512-dvO5M52v7m7Dy96+XUnzXNsQ/0npsYpU6dL205kAtEDueswoz3aU3bh1UMoK4cQmcGtB1YRyLKqp+DXi05lzFg==" + } + } + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -4587,6 +4679,16 @@ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", "dev": true }, + "@sapphire/async-queue": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.1.tgz", + "integrity": "sha512-FFTlPOWZX1kDj9xCAsRzH5xEJfawg1lNoYAA+ecOWJMHOfiZYb1uXOI3ne9U4UILSEPwfE68p3T9wUHwIQfR0g==" + }, + "@sapphire/snowflake": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.2.2.tgz", + "integrity": "sha512-ula2O0kpSZtX9rKXNeQMrHwNd7E4jPDJYUXmEGTFdMRfyfMw+FPyh04oKMjAiDuOi64bYgVkOV3MjK+loImFhQ==" + }, "@sindresorhus/is": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", @@ -5439,6 +5541,11 @@ } } }, + "discord-api-types": { + "version": "0.36.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.36.1.tgz", + "integrity": "sha512-PTDinUU574hXA9Ko9wrftL1iww1raNiRVKjuPIWQ5Li1g7vQPArpZWw9x01kh/IXLPdzSAJ6H8T0eAYzxzFzIg==" + }, "discord-oauth2": { "version": "git+ssh://git@github.com/ryanblenis/discord-oauth2.git#ca0093d5b34efdd6ec2ce45a4a2678b3282cea31", "integrity": "sha512-J3w3djV6I4PtnLp3lZga4m41DPjqXtyETV8rMG9FSIev5i5mxpkrsfXRni3Jc+qYxH86LcLa7zUwnWHHXpcu0g==", @@ -7632,6 +7739,11 @@ "restler": "~3.3.0" } }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, "tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", @@ -7703,6 +7815,11 @@ "xtend": "^4.0.1" } }, + "undici": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.6.1.tgz", + "integrity": "sha512-yYVqywdCbNb99f/w045wqmv++WExXDjY0FEvLSB7QUZZH6izxrVkF4dJn1aimcvN0+WAhv75Gg7v6VJoqmRtJQ==" + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 590c825..a7769cb 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,12 @@ }, "homepage": "https://github.com/PretendoNetwork/website#readme", "dependencies": { + "@discordjs/rest": "^0.5.0", "adm-zip": "^0.5.9", "browserify": "^17.0.0", "colors": "^1.4.0", "cookie-parser": "^1.4.5", + "discord-api-types": "^0.36.1", "discord-oauth2": "github:ryanblenis/discord-oauth2", "express": "^4.17.1", "express-handlebars": "^5.3.1", diff --git a/public/assets/css/account.css b/public/assets/css/account.css index 1b5a6b2..715076d 100644 --- a/public/assets/css/account.css +++ b/public/assets/css/account.css @@ -1,3 +1,7 @@ +body.modal-open { + overflow: hidden; +} + /* Removing until it's done */ .setting-card a.edit, .sign-in-history a { @@ -249,6 +253,99 @@ fieldset { background: #a9375b; } +div.online-files-modal-wrapper { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + + display: flex; + justify-content: center; + align-items: center; + background: rgba(0, 0, 0, 0.6); + + z-index: 10; +} +div.online-files-modal-wrapper.hidden { + display: none; +} +div.online-files-modal { + background: #393b5f; + padding: 48px; + border-radius: 8px; + text-align: left; + width: min(660px, 90%); + box-sizing: border-box; +} + +div.online-files-modal h1 { + margin-top: 0; +} +p.online-files-modal-caption { + color: var(--text-secondary); +} +p.online-files-modal-caption span { + color: var(--text); +} + +.online-files-modal-button-wrapper { + margin-top: 24px; + display: flex; + justify-content: flex-end; +} +.online-files-modal-button-wrapper button { + margin-left: 12px; + width: fit-content; +} +.online-files-modal-button-wrapper button.cancel { + background: none; +} +.online-files-modal-button-wrapper button.confirm { + padding: 12px 24px; +} + +button { + appearance: none; + -webkit-appearance: none; + display: block; + font-family: Poppins, Arial, Helvetica, sans-serif; + font-size: 1rem; + height: fit-content; + + background: var(--btn); + border: none; + border-radius: 4px; + padding: 12px; + color: var(--text); + width: 100%; + + transition: filter 300ms; + pointer-events: all; + cursor: pointer; + filter: none; +} + +input { + appearance: none; + -webkit-appearance: none; + display: block; + font-family: Poppins, Arial, Helvetica, sans-serif; + font-size: 1rem; + background-color: black; /*change me*/ + border: none; + border-radius: 4px; + padding: 12px; + color: var(--text); + width: calc(100% - 24px); +} + +input:focus { + background-color: #4b5595; + outline: none; + transition: 150ms; +} + footer { margin-top: 80px; } diff --git a/public/assets/css/upgrade.css b/public/assets/css/upgrade.css index 2e0229d..87885ab 100644 --- a/public/assets/css/upgrade.css +++ b/public/assets/css/upgrade.css @@ -13,6 +13,9 @@ width: 1600px; height: 1400px; } +body.modal-open { + overflow: hidden; +} .back-arrow { position: absolute; @@ -261,7 +264,7 @@ form button.unsubscribe:hover { div.unsub-modal-wrapper, div.switch-tier-modal-wrapper { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; diff --git a/public/assets/js/account.js b/public/assets/js/account.js index 2e6da46..a73794f 100644 --- a/public/assets/js/account.js +++ b/public/assets/js/account.js @@ -19,4 +19,43 @@ document.getElementById('remove-discord-connection')?.addEventListener('click', } }) .catch(console.log); +}); + +const onlineFilesModal = document.querySelector('.online-files-modal-wrapper'); +const onlineFilesModalButtonConfirm = document.getElementById('onlineFilesConfirmButton'); +const onlineFilesModalButtonClose = document.getElementById('onlineFilesCloseButton'); +const onlineFilesModalPasswordInput = document.getElementById('password'); + +document.getElementById('download-cemu-files')?.addEventListener('click', event => { + event.preventDefault(); + + onlineFilesModal.classList.remove('hidden'); +}); + +onlineFilesModalButtonConfirm?.addEventListener('click', () => { + fetch('/account/online-files', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + password: onlineFilesModalPasswordInput.value + }) + }) + .then(response => response.blob()) + .then(blob => URL.createObjectURL(blob)) + .then(blobUrl => { + const a = document.createElement('a'); + a.href = blobUrl; + a.setAttribute('download', 'Cemu Pretendo Online Files.zip'); + a.click(); + + onlineFilesModal.classList.add('hidden'); + }) + .catch(console.log); +}); + +onlineFilesModalButtonClose?.addEventListener('click', () => { + onlineFilesModal.classList.add('hidden'); }); \ No newline at end of file diff --git a/public/assets/js/upgrade.js b/public/assets/js/upgrade.js index a652d6a..bc6b451 100644 --- a/public/assets/js/upgrade.js +++ b/public/assets/js/upgrade.js @@ -70,6 +70,7 @@ buttons.submit.addEventListener('click', function(e) { oldTierNameSpan.innerText = currentTierElement.dataset.tierName; newTierNameSpan.innerText = document.querySelector('input[name="tier"]:checked').dataset.tierName; + document.body.classList.add('modal-open'); document.querySelector('.switch-tier-modal-wrapper').classList.remove('hidden'); } else { submitForm(); @@ -83,12 +84,14 @@ buttons.unsubModal.show.addEventListener('click', function(e) { tierNameSpan.innerText = currentTierElement.dataset.tierName; // Show the unsubscribe modal + document.body.classList.add('modal-open'); document.querySelector('.unsub-modal-wrapper').classList.remove('hidden'); }); buttons.unsubModal.close.addEventListener('click', function(e) { e.preventDefault(); // Hide the unsubscribe modal + document.body.classList.remove('modal-open'); document.querySelector('.unsub-modal-wrapper').classList.add('hidden'); }); buttons.unsubModal.confirm.addEventListener('click', function(e) { @@ -101,6 +104,7 @@ buttons.switchTierModal.close.addEventListener('click', function(e) { e.preventDefault(); // Hide the switch tier modal + document.body.classList.remove('modal-open'); document.querySelector('.switch-tier-modal-wrapper').classList.add('hidden'); }); buttons.switchTierModal.confirm.addEventListener('click', function(e) { diff --git a/src/middleware/redirect.js b/src/middleware/redirect.js new file mode 100644 index 0000000..7ca4fb7 --- /dev/null +++ b/src/middleware/redirect.js @@ -0,0 +1,13 @@ +async function redirectMiddleware(request, response, next) { + if (request.method === 'POST') { + request.redirect = request.body.redirect?.startsWith('/') ? request.body.redirect : null; + } + + if (request.query.redirect) { + response.locals.redirect = request.query.redirect?.startsWith('/') ? request.query.redirect : null; + } + + return next(); +} + +module.exports = redirectMiddleware; \ No newline at end of file diff --git a/src/middleware/render-data.js b/src/middleware/render-data.js new file mode 100644 index 0000000..38bcf14 --- /dev/null +++ b/src/middleware/render-data.js @@ -0,0 +1,43 @@ +const util = require('../util'); +const database = require('../database'); + +async function renderDataMiddleware(request, response, next) { + if (request.path.startsWith('/assets')) { + return next(); + } + + // Get user local + const reqLocale = request.locale; + const locale = util.getLocale(reqLocale.region, reqLocale.language); + + response.locals.locale = locale; + response.locals.localeString = reqLocale.toString(); + + // Get message cookies + response.locals.success_message = request.cookies.success_message; + response.locals.error_message = request.cookies.error_message; + + // Reset message cookies + response.clearCookie('success_message', { domain: '.pretendo.network' }); + response.clearCookie('error_message', { domain: '.pretendo.network' }); + + response.locals.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token; + + if (response.locals.isLoggedIn) { + try { + response.locals.account = await util.getUserAccountData(request, response); + + request.pnid = await database.PNID.findOne({ pid: response.locals.account.pid }); + request.account = response.locals.account; + + return next(); + } catch (error) { + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + return response.redirect('/account/login'); + } + } else { + return next(); + } +} + +module.exports = renderDataMiddleware; \ No newline at end of file diff --git a/src/middleware/require-login.js b/src/middleware/require-login.js new file mode 100644 index 0000000..873e2e8 --- /dev/null +++ b/src/middleware/require-login.js @@ -0,0 +1,10 @@ +async function requireLoginMiddleware(request, response, next) { + // Verify the user is logged in + if (!request.cookies.access_token || !request.cookies.refresh_token) { + return response.redirect(`/account/login?redirect=${request.originalUrl}`); + } + + return next(); +} + +module.exports = requireLoginMiddleware; \ No newline at end of file diff --git a/src/routers/account.js b/src/routers/account.js deleted file mode 100644 index 202ebd5..0000000 --- a/src/routers/account.js +++ /dev/null @@ -1,823 +0,0 @@ -const express = require('express'); -const crypto = require('crypto'); -const DiscordOauth2 = require('discord-oauth2'); -const { v4: uuidv4 } = require('uuid'); -const AdmZip = require('adm-zip'); -const Stripe = require('stripe'); -const database = require('../database'); -const cache = require('../cache'); -const util = require('../util'); -const logger = require('../logger'); -const config = require('../../config.json'); - -const { Router } = express; -const aesKey = Buffer.from(config.aes_key, 'hex'); - -const stripe = new Stripe(config.stripe.secret_key); -const router = new Router(); - -// Create OAuth client -const discordOAuth = new DiscordOauth2({ - clientId: config.discord.client_id, - clientSecret: config.discord.client_secret, - redirectUri: `${config.http.base_url}/account/connect/discord`, - version: 'v9' -}); - -router.get('/', 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'); - } - - // Setup the data to be sent to the handlebars renderer - const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - success: request.cookies.success, - error: request.cookies.error - }; - - // Reset message cookies - response.clearCookie('success', { domain: '.pretendo.network' }); - response.clearCookie('error', { domain: '.pretendo.network' }); - - // Check for Stripe messages - const { upgrade_success } = request.query; - - if (upgrade_success === 'true') { - renderData.success = 'Account upgraded successfully'; - } else if (upgrade_success === 'false') { - renderData.error = 'Account upgrade failed'; - } - - // 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' - }); - } - - // Set user account info to render data - const account = apiResponse.body; - const pid = account.pid; - - const pnid = await database.PNID.findOne({ pid }); - - renderData.tierName = pnid.get('connections.stripe.tier_name'); - renderData.tierLevel = pnid.get('connections.stripe.tier_level'); - renderData.account = account; - renderData.isTester = account.access_level > 0; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - // Check if a Discord account is linked to the PNID - if (account.connections.discord.id && account.connections.discord.id.trim() !== '') { - // If Discord account is linked, then get user info - try { - renderData.discordUser = await discordOAuth.getUser(account.connections.discord.access_token); - } catch (error) { - // Assume expired, refresh and retry Discord request - let tokens; - try { - tokens = await discordOAuth.tokenRequest({ - scope: 'identify guilds', - grantType: 'refresh_token', - refreshToken: account.connections.discord.refresh_token, - }); - } catch (error) { - renderData.error = 'Invalid Discord refresh token. Remove account and relink'; - return response.render('account/account', renderData); - } - - // TODO: Add a dedicated endpoint for updating connections? - apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', { - 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}` - }, { - data: { - id: account.connections.discord.id, - access_token: tokens.access_token, - refresh_token: tokens.refresh_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.apiPostGetRequest('/v1/connections/add/discord', { - 'Authorization': `${tokens.token_type} ${tokens.access_token}` - }, { - data: { - id: account.connections.discord.id, - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - } - }); - - // If still failed, something went horribly wrong - if (apiResponse.statusCode !== 200) { - // TODO: Error message - return response.status(apiResponse.statusCode).json({ - error: 'Bad' - }); - } - } - - account.connections.discord.access_token = tokens.access_token; - account.connections.discord.refresh_token = tokens.refresh_token; - } - - // Get the users Discord roles to check if they are a tester - const { roles } = await discordOAuth.getMemberRolesForGuild({ - userId: account.connections.discord.id, - guildId: config.discord.guild_id, - botToken: config.discord.bot_token - }); - - // Only run this check if not already a tester (edge case) - if (!renderData.isTester) { - // 409116477212459008 = Developer - // 882247322933801030 = Super Mario (Patreon tier) - renderData.isTester = roles.some(role => config.discord.tester_roles.includes(role)); - } - } else { - // If no Discord account linked, generate an auth URL - const discordAuthURL = discordOAuth.generateAuthUrl({ - scope: ['identify', 'guilds'], - state: crypto.randomBytes(16).toString('hex'), - }); - - renderData.discordAuthURL = discordAuthURL; - } - - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - response.render('account/account', renderData); -}); - -router.get('/login', async (request, response) => { - const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - error: request.cookies.error - }; - - renderData.redirect = request.query.redirect; - - response.clearCookie('error', { domain: '.pretendo.network' }); - - response.render('account/login', renderData); -}); - -router.post('/login', async (request, response) => { - const { username, password } = request.body; - let { redirect } = request.body; - - let apiResponse = await util.apiPostGetRequest('/v1/login', {}, { - username, - password, - grant_type: 'password' - }); - - if (apiResponse.statusCode !== 200) { - response.cookie('error', apiResponse.body.error, { domain: '.pretendo.network' }); - return response.redirect('/account/login'); - } - - 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}` - }); - - 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' }); - - if (!redirect.startsWith('/')) { - redirect = null; - } - - response.redirect(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' }); - - let redirect = request.query.redirect; - if (!redirect.startsWith('/')) { - redirect = null; - } - - renderData.redirect = redirect; - - response.render('account/register', renderData); -}); - -router.post('/register', async (request, response) => { - const { email, username, mii_name, password, password_confirm, 'h-captcha-response': hCaptchaResponse } = request.body; - let { redirect } = 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, hCaptchaResponse - }); - - 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' }); - - if (!redirect.startsWith('/')) { - redirect = null; - } - - response.redirect(redirect || '/account'); -}); - -router.get('/logout', async(_request, response) => { - response.clearCookie('refresh_token', { domain: '.pretendo.network' }); - response.clearCookie('access_token', { domain: '.pretendo.network' }); - response.clearCookie('token_type', { domain: '.pretendo.network' }); - response.clearCookie('ph', { domain: '.pretendo.network' }); - - response.redirect('/'); -}); - -router.get('/connect/discord', async (request, response) => { - let tokens; - try { - // Attempt to get OAuth2 tokens - tokens = await discordOAuth.tokenRequest({ - code: request.query.code, - scope: 'identify guilds', - grantType: 'authorization_code', - }); - } catch (error) { - response.cookie('error', 'Invalid Discord authorization code. Please try again', { domain: '.pretendo.network' }); - return response.redirect('/account'); - } - - // Get Discord user data - const user = await discordOAuth.getUser(tokens.access_token); - - // Link the Discord account to the PNID - let apiResponse = await util.apiPostGetRequest('/v1/connections/add/discord', { - 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}` - }, { - data: { - id: user.id, - access_token: tokens.access_token, - refresh_token: tokens.refresh_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.apiPostGetRequest('/v1/connections/add/discord', { - 'Authorization': `${tokens.token_type} ${tokens.access_token}` - }, { - data: { - id: user.id, - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - } - }); - - // If still failed, something went horribly wrong - if (apiResponse.statusCode !== 200) { - // TODO: Error message - return response.status(apiResponse.statusCode).json({ - error: 'Bad' - }); - } - } - - response.cookie('success', 'Discord account linked successfully', { 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?redirect=/online-files'); - } - - // 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()); -}); - -router.get('/miieditor', async (request, response) => { - - const reqLocale = request.locale; - const locale = util.getLocale(reqLocale.region, reqLocale.language); - - // Should obviously be the user's - const encodedUserMiiData = 'AwAAQOlVognnx0GC2X0LLQOzuI0n2QAAAUBiAGUAbABsAGEAAABFAAAAAAAAAEBAEgCBAQRoQxggNEYUgRIXaA0AACkDUkhQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP6G'; - - // Adapted from https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values - - const editorToHex = { - 'face': [ - 0x00,0x01,0x08,0x02,0x03,0x09,0x04,0x05,0x0a,0x06,0x07,0x0b - ], - 'hairs': [ - [0x21,0x2f,0x28,0x25,0x20,0x6b,0x30,0x33,0x37,0x46,0x2c,0x42], - [0x34,0x32,0x26,0x31,0x2b,0x1f,0x38,0x44,0x3e,0x73,0x4c,0x77], - [0x40,0x51,0x74,0x79,0x16,0x3a,0x3c,0x57,0x7d,0x75,0x49,0x4b], - [0x2a,0x59,0x39,0x36,0x50,0x22,0x17,0x56,0x58,0x76,0x27,0x24], - [0x2d,0x43,0x3b,0x41,0x29,0x1e,0x0c,0x10,0x0a,0x52,0x80,0x81], - [0x0e,0x5f,0x69,0x64,0x06,0x14,0x5d,0x66,0x1b,0x04,0x11,0x6e], - [0x7b,0x08,0x6a,0x48,0x03,0x15,0x00,0x62,0x3f,0x5a,0x0b,0x78], - [0x05,0x4a,0x6c,0x5e,0x7c,0x19,0x63,0x45,0x23,0x0d,0x7a,0x71], - [0x35,0x18,0x55,0x53,0x47,0x83,0x60,0x65,0x1d,0x07,0x0f,0x70], - [0x4f,0x01,0x6d,0x7f,0x5b,0x1a,0x3d,0x67,0x02,0x4d,0x12,0x5c], - [0x54,0x09,0x13,0x82,0x61,0x68,0x2e,0x4e,0x1c,0x72,0x7e,0x6f] - ], - 'eyebrows': [ - [0x06,0x00,0x0c,0x01,0x09,0x13,0x07,0x15,0x08,0x11,0x05,0x04], - [0x0b,0x0a,0x02,0x03,0x0e,0x14,0x0f,0x0d,0x16,0x12,0x10,0x17] - ], - 'eyes': [ - [0x02,0x04,0x00,0x08,0x27,0x11,0x01,0x1a,0x10,0x0f,0x1b,0x14], - [0x21,0x0b,0x13,0x20,0x09,0x0c,0x17,0x22,0x15,0x19,0x28,0x23], - [0x05,0x29,0x0d,0x24,0x25,0x06,0x18,0x1e,0x1f,0x12,0x1c,0x2e], - [0x07,0x2c,0x26,0x2a,0x2d,0x1d,0x03,0x2b,0x16,0x0a,0x0e,0x2f], - [0x30,0x31,0x32,0x35,0x3b,0x38,0x36,0x3a,0x39,0x37,0x33,0x34] - ], - 'nose': [ - [0x01,0x0a,0x02,0x03,0x06,0x00, 0x05,0x04,0x08,0x09,0x07,0x0B], - [0x0d,0x0e,0x0c,0x11,0x10,0x0f] - ], - 'mouth': [ - [0x17,0x01,0x13,0x15,0x16,0x05,0x00,0x08,0x0a,0x10,0x06,0x0d], - [0x07,0x09,0x02,0x11,0x03,0x04,0x0f,0x0b,0x14,0x12,0x0e,0x0c], - [0x1b,0x1e,0x18,0x19,0x1d,0x1c,0x1a,0x23,0x1f,0x22,0x21,0x20] - ] - }; - - response.render('account/miieditor', { - layout: 'main', - locale, - localeString: reqLocale.toString(), - encodedUserMiiData, - editorToHex - }); -}); - -router.get('/upgrade', 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?redirect=/account/upgrade'); - } - - // 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) { - return response.redirect('/account/login'); - } - - 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) { - return response.redirect('/account/login'); - } - - // Set user account info to render data - const account = apiResponse.body; - const pid = account.pid; - - const pnid = await database.PNID.findOne({ pid }); - - const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - error: request.cookies.error, - currentTier: pnid.get('connections.stripe.price_id'), - donationCache: await cache.getStripeDonationCache() - }; - - const { data: prices } = await stripe.prices.list(); - const { data: products } = await stripe.products.list(); - - renderData.tiers = products - .filter(product => product.active) - .sort((a, b) => +a.metadata.tier_level - +b.metadata.tier_level) - .map(product => { - const price = prices.find(price => price.product === product.id); - const perks = []; - - if (product.metadata.discord_read === 'true') { - perks.push('Read-only access to select dev channels on Discord'); - } - - if (product.metadata.beta === 'true') { - perks.push('Access the beta servers'); - } - - return { - price_id: price.id, - thumbnail: product.images[0], - name: product.name, - description: product.description, - perks, - price: (price.unit_amount / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }), - }; - }); - - response.render('account/upgrade', renderData); -}); - -router.post('/stripe/checkout/:priceId', 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) { - return response.redirect('/account/login'); - } - - 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) { - return response.redirect('/account/login'); - } - - // Set user account info to render data - const account = apiResponse.body; - const pid = account.pid; - - let customer; - const { data: searchResults } = await stripe.customers.search({ - query: `metadata['pnid_pid']:'${pid}'` - }); - - if (searchResults.length !== 0) { - customer = searchResults[0]; - } else { - customer = await stripe.customers.create({ - email: account.email.address, - metadata: { - pnid_pid: pid - } - }); - } - - await database.PNID.updateOne({ pid }, { - $set: { - 'connections.stripe.customer_id': customer.id // ensure PNID always has latest customer ID - } - }, { upsert: true }).exec(); - - const priceId = request.params.priceId; - - const pnid = await database.PNID.findOne({ pid }); - - if (pnid.get('access_level') >= 2) { - response.cookie('error', 'Staff members do not need to purchase tiers', { domain: '.pretendo.network' }); - return response.redirect('/account'); - } - - try { - const session = await stripe.checkout.sessions.create({ - line_items: [ - { - price: priceId, - quantity: 1, - }, - ], - customer: customer.id, - mode: 'subscription', - success_url: `${config.http.base_url}/account?upgrade_success=true`, - cancel_url: `${config.http.base_url}/account?upgrade_success=false` - }); - - return response.redirect(303, session.url); - } catch (error) { - // Maybe we need a dedicated error page? - // Or handle this as not cookies? - response.cookie('error', error.message, { domain: '.pretendo.network' }); - return response.redirect('/account'); - } -}); - -router.post('/stripe/unsubscribe', 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) { - return response.redirect('/account/login'); - } - - 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) { - return response.redirect('/account/login'); - } - - // Set user account info to render data - const account = apiResponse.body; - const pid = account.pid; - - const pnid = await database.PNID.findOne({ pid }); - const subscriptionId = pnid.get('connections.stripe.subscription_id'); - const tierName = pnid.get('connections.stripe.tier_name'); - - if (subscriptionId) { - try { - await stripe.subscriptions.del(subscriptionId); - - const updateData = { - 'connections.stripe.subscription_id': null, - 'connections.stripe.price_id': null, - 'connections.stripe.tier_level': 0, - 'connections.stripe.tier_name': null, - }; - - if (pnid.get('access_level') < 2) { - // Fail-safe for if staff members reach here - // Mostly only useful during testing - updateData.access_level = 0; - } - - await database.PNID.updateOne({ pid }, { $set: updateData }).exec(); - } catch (error) { - logger.error(`Error canceling old user subscription | ${pnid.get('connections.stripe.customer_id')}, ${pid}, ${subscriptionId} | - ${error.message}`); - - response.cookie('error', 'Error canceling subscription! Contact support if issue persists', { domain: '.pretendo.network' }); - - return response.redirect('/account'); - } - } - - response.cookie('success', `Unsubscribed from ${tierName}`, { domain: '.pretendo.network' }); - return response.redirect('/account'); -}); - -router.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (request, response) => { - const stripeSignature = request.headers['stripe-signature']; - let event; - - try { - event = stripe.webhooks.constructEvent(request.body, stripeSignature, config.stripe.webhook_secret); - } catch (err) { - return response.status(400).send(`Webhook Error: ${err.message}`); - } - - await util.handleStripeEvent(event); - - response.json({ received: true }); -}); - - -module.exports = router; \ No newline at end of file diff --git a/src/routers/aprilfools.js b/src/routers/aprilfools.js deleted file mode 100644 index 818070f..0000000 --- a/src/routers/aprilfools.js +++ /dev/null @@ -1,23 +0,0 @@ -const { Router } = require('express'); -const util = require('../util'); -const router = new Router(); - -router.get('/', async (request, response) => { - - const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - }; - - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - - response.render('aprilfools', renderData); -}); - -module.exports = router; diff --git a/src/routers/localization.js b/src/routers/localization.js deleted file mode 100644 index c4fee3c..0000000 --- a/src/routers/localization.js +++ /dev/null @@ -1,22 +0,0 @@ -const { Router } = require('express'); -const util = require('../util'); -const router = new Router(); - -router.get('/', async (request, response) => { - const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - }; - - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - - response.render('localization', renderData); -}); - -module.exports = router; diff --git a/src/routes/account.js b/src/routes/account.js new file mode 100644 index 0000000..6ac84f9 --- /dev/null +++ b/src/routes/account.js @@ -0,0 +1,424 @@ +const express = require('express'); +const crypto = require('crypto'); +const DiscordOauth2 = require('discord-oauth2'); +const { v4: uuidv4 } = require('uuid'); +const AdmZip = require('adm-zip'); +const Stripe = require('stripe'); +const { REST: DiscordRest } = require('@discordjs/rest'); +const { Routes: DiscordRoutes } = require('discord-api-types/v10'); +const requireLoginMiddleware = require('../middleware/require-login'); +const database = require('../database'); +const cache = require('../cache'); +const util = require('../util'); +const logger = require('../logger'); +const config = require('../../config.json'); + +const { Router } = express; + +const stripe = new Stripe(config.stripe.secret_key); +const router = new Router(); +const discordRest = new DiscordRest({ version: '10' }).setToken(config.discord.bot_token); + +// Create OAuth client +const discordOAuth = new DiscordOauth2({ + clientId: config.discord.client_id, + clientSecret: config.discord.client_secret, + redirectUri: `${config.http.base_url}/account/connect/discord`, + version: 'v10' +}); + +router.get('/', requireLoginMiddleware, async (request, response) => { + // Setup the data to be sent to the handlebars renderer + const renderData = {}; + + // Check for Stripe messages + const { upgrade_success } = request.query; + + if (upgrade_success === 'true') { + renderData.success_message = 'Account upgraded successfully'; + } else if (upgrade_success === 'false') { + renderData.error_message = 'Account upgrade failed'; + } + + const { account } = request; + const { pnid } = request; + + renderData.tierName = pnid.get('connections.stripe.tier_name'); + renderData.tierLevel = pnid.get('connections.stripe.tier_level'); + renderData.account = account; + renderData.isTester = account.access_level > 0; + + // Check if a Discord account is linked to the PNID + if (account.connections.discord.id && account.connections.discord.id.trim() !== '') { + try { + renderData.discordUser = await discordRest.get(DiscordRoutes.user(account.connections.discord.id)); + } catch (error) { + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + } + } else { + // If no Discord account linked, generate an auth URL + const discordAuthURL = discordOAuth.generateAuthUrl({ + scope: ['identify', 'guilds'], + state: crypto.randomBytes(16).toString('hex'), + }); + + renderData.discordAuthURL = discordAuthURL; + } + + response.render('account/account', renderData); +}); + +router.get('/login', async (request, response) => { + response.render('account/login'); +}); + +router.post('/login', async (request, response) => { + const { username, password } = request.body; + + try { + const tokens = await util.login(username, password); + + 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.redirect(request.redirect || '/account'); + + } catch (error) { + console.log(error); + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + return response.redirect('/account/login'); + } +}); + +router.get('/register', async (request, response) => { + const renderData = { + email: request.cookies.email, + username: request.cookies.username, + mii_name: request.cookies.mii_name, + }; + + 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, 'h-captcha-response': hCaptchaResponse } = 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' }); + + try { + const tokens = await util.register({ + email, + username, + mii_name, + password, + password_confirm, + hCaptchaResponse + }); + + 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('email', { domain: '.pretendo.network' }); + response.clearCookie('username', { domain: '.pretendo.network' }); + response.clearCookie('mii_name', { domain: '.pretendo.network' }); + + response.redirect(request.redirect || '/account'); + } catch (error) { + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + return response.redirect('/account/register'); + } +}); + +router.get('/logout', async(_request, response) => { + response.clearCookie('refresh_token', { domain: '.pretendo.network' }); + response.clearCookie('access_token', { domain: '.pretendo.network' }); + response.clearCookie('token_type', { domain: '.pretendo.network' }); + + response.redirect('/'); +}); + +router.get('/connect/discord', requireLoginMiddleware, async (request, response) => { + let tokens; + try { + // Attempt to get OAuth2 tokens + tokens = await discordOAuth.tokenRequest({ + code: request.query.code, + scope: 'identify guilds', + grantType: 'authorization_code', + }); + } catch (error) { + response.cookie('error_message', 'Invalid Discord authorization code. Please try again', { domain: '.pretendo.network' }); + return response.redirect('/account'); + } + + // Get Discord user data + const discordUser = await discordOAuth.getUser(tokens.access_token); + + try { + await util.updateDiscordConnection(discordUser, request, response); + + response.cookie('success_message', 'Discord account linked successfully', { domain: '.pretendo.network' }); + response.redirect('/account'); + } catch (error) { + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + return response.redirect('/account'); + } +}); + +router.post('/online-files', requireLoginMiddleware, async (request, response) => { + const { account } = request; + const { password } = request.body; + + const hashedPassword = util.nintendoPasswordHash(password, account.pid); + + 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=${hashedPassword}`; + + 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="Cemu Pretendo Online Files.zip'); + response.set('Content-Type', 'application/zip'); + + response.end(onlineFiles.toBuffer()); +}); + +router.get('/miieditor', requireLoginMiddleware, async (request, response) => { + const { account } = request; + + // Adapted from https://www.3dbrew.org/wiki/Mii#Mapped_Editor_.3C-.3E_Hex_values + + const editorToHex = { + 'face': [ + 0x00,0x01,0x08,0x02,0x03,0x09,0x04,0x05,0x0a,0x06,0x07,0x0b + ], + 'hairs': [ + [0x21,0x2f,0x28,0x25,0x20,0x6b,0x30,0x33,0x37,0x46,0x2c,0x42], + [0x34,0x32,0x26,0x31,0x2b,0x1f,0x38,0x44,0x3e,0x73,0x4c,0x77], + [0x40,0x51,0x74,0x79,0x16,0x3a,0x3c,0x57,0x7d,0x75,0x49,0x4b], + [0x2a,0x59,0x39,0x36,0x50,0x22,0x17,0x56,0x58,0x76,0x27,0x24], + [0x2d,0x43,0x3b,0x41,0x29,0x1e,0x0c,0x10,0x0a,0x52,0x80,0x81], + [0x0e,0x5f,0x69,0x64,0x06,0x14,0x5d,0x66,0x1b,0x04,0x11,0x6e], + [0x7b,0x08,0x6a,0x48,0x03,0x15,0x00,0x62,0x3f,0x5a,0x0b,0x78], + [0x05,0x4a,0x6c,0x5e,0x7c,0x19,0x63,0x45,0x23,0x0d,0x7a,0x71], + [0x35,0x18,0x55,0x53,0x47,0x83,0x60,0x65,0x1d,0x07,0x0f,0x70], + [0x4f,0x01,0x6d,0x7f,0x5b,0x1a,0x3d,0x67,0x02,0x4d,0x12,0x5c], + [0x54,0x09,0x13,0x82,0x61,0x68,0x2e,0x4e,0x1c,0x72,0x7e,0x6f] + ], + 'eyebrows': [ + [0x06,0x00,0x0c,0x01,0x09,0x13,0x07,0x15,0x08,0x11,0x05,0x04], + [0x0b,0x0a,0x02,0x03,0x0e,0x14,0x0f,0x0d,0x16,0x12,0x10,0x17] + ], + 'eyes': [ + [0x02,0x04,0x00,0x08,0x27,0x11,0x01,0x1a,0x10,0x0f,0x1b,0x14], + [0x21,0x0b,0x13,0x20,0x09,0x0c,0x17,0x22,0x15,0x19,0x28,0x23], + [0x05,0x29,0x0d,0x24,0x25,0x06,0x18,0x1e,0x1f,0x12,0x1c,0x2e], + [0x07,0x2c,0x26,0x2a,0x2d,0x1d,0x03,0x2b,0x16,0x0a,0x0e,0x2f], + [0x30,0x31,0x32,0x35,0x3b,0x38,0x36,0x3a,0x39,0x37,0x33,0x34] + ], + 'nose': [ + [0x01,0x0a,0x02,0x03,0x06,0x00, 0x05,0x04,0x08,0x09,0x07,0x0B], + [0x0d,0x0e,0x0c,0x11,0x10,0x0f] + ], + 'mouth': [ + [0x17,0x01,0x13,0x15,0x16,0x05,0x00,0x08,0x0a,0x10,0x06,0x0d], + [0x07,0x09,0x02,0x11,0x03,0x04,0x0f,0x0b,0x14,0x12,0x0e,0x0c], + [0x1b,0x1e,0x18,0x19,0x1d,0x1c,0x1a,0x23,0x1f,0x22,0x21,0x20] + ] + }; + + response.render('account/miieditor', { + encodedUserMiiData: account.mii.data, + editorToHex + }); +}); + +router.get('/upgrade', requireLoginMiddleware, async (request, response) => { + // Set user account info to render data + const { pnid } = request; + + const renderData = { + error: request.cookies.error, + currentTier: pnid.get('connections.stripe.price_id'), + donationCache: await cache.getStripeDonationCache() + }; + + const { data: prices } = await stripe.prices.list(); + const { data: products } = await stripe.products.list(); + + renderData.tiers = products + .filter(product => product.active) + .sort((a, b) => +a.metadata.tier_level - +b.metadata.tier_level) + .map(product => { + const price = prices.find(price => price.product === product.id); + const perks = []; + + if (product.metadata.discord_read === 'true') { + perks.push('Read-only access to select dev channels on Discord'); + } + + if (product.metadata.beta === 'true') { + perks.push('Access the beta servers'); + } + + return { + price_id: price.id, + thumbnail: product.images[0], + name: product.name, + description: product.description, + perks, + price: (price.unit_amount / 100).toLocaleString('en-US', { style: 'currency', currency: 'USD' }), + }; + }); + + response.render('account/upgrade', renderData); +}); + +router.post('/stripe/checkout/:priceId', requireLoginMiddleware, async (request, response) => { + // Set user account info to render data + const { account } = request; + const pid = account.pid; + + let customer; + const { data: searchResults } = await stripe.customers.search({ + query: `metadata['pnid_pid']:'${pid}'` + }); + + if (searchResults.length !== 0) { + customer = searchResults[0]; + } else { + customer = await stripe.customers.create({ + email: account.email.address, + metadata: { + pnid_pid: pid + } + }); + } + + await database.PNID.updateOne({ pid }, { + $set: { + 'connections.stripe.customer_id': customer.id, // ensure PNID always has latest customer ID + 'connections.stripe.latest_webhook_timestamp': 0 + } + }, { upsert: true }).exec(); + + const priceId = request.params.priceId; + + const pnid = await database.PNID.findOne({ pid }); + + if (pnid.get('access_level') >= 2) { + response.cookie('error_message', 'Staff members do not need to purchase tiers', { domain: '.pretendo.network' }); + return response.redirect('/account'); + } + + try { + const session = await stripe.checkout.sessions.create({ + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + customer: customer.id, + mode: 'subscription', + success_url: `${config.http.base_url}/account?upgrade_success=true`, + cancel_url: `${config.http.base_url}/account?upgrade_success=false` + }); + + return response.redirect(303, session.url); + } catch (error) { + // Maybe we need a dedicated error page? + // Or handle this as not cookies? + response.cookie('error_message', error.message, { domain: '.pretendo.network' }); + + return response.redirect('/account'); + } +}); + +router.post('/stripe/unsubscribe', requireLoginMiddleware, async (request, response) => { + // Set user account info to render data + const { pnid } = request; + + const pid = pnid.get('pid'); + const subscriptionId = pnid.get('connections.stripe.subscription_id'); + const tierName = pnid.get('connections.stripe.tier_name'); + + if (subscriptionId) { + try { + await stripe.subscriptions.del(subscriptionId); + + const updateData = { + 'connections.stripe.subscription_id': null, + 'connections.stripe.price_id': null, + 'connections.stripe.tier_level': 0, + 'connections.stripe.tier_name': null, + }; + + if (pnid.get('access_level') < 2) { + // Fail-safe for if staff members reach here + // Mostly only useful during testing + updateData.access_level = 0; + } + + await database.PNID.updateOne({ pid }, { $set: updateData }).exec(); + } catch (error) { + logger.error(`Error canceling old user subscription | ${pnid.get('connections.stripe.customer_id')}, ${pid}, ${subscriptionId} | - ${error.message}`); + + response.cookie('error_message', 'Error canceling subscription! Contact support if issue persists', { domain: '.pretendo.network' }); + + return response.redirect('/account'); + } + } + + response.cookie('success', `Unsubscribed from ${tierName}`, { domain: '.pretendo.network' }); + return response.redirect('/account'); +}); + +router.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (request, response) => { + const stripeSignature = request.headers['stripe-signature']; + let event; + + try { + event = stripe.webhooks.constructEvent(request.body, stripeSignature, config.stripe.webhook_secret); + } catch (err) { + return response.status(400).send(`Webhook Error: ${err.message}`); + } + + await util.handleStripeEvent(event); + + response.json({ received: true }); +}); + + +module.exports = router; \ No newline at end of file diff --git a/src/routes/aprilfools.js b/src/routes/aprilfools.js new file mode 100644 index 0000000..94bdb4b --- /dev/null +++ b/src/routes/aprilfools.js @@ -0,0 +1,8 @@ +const { Router } = require('express'); +const router = new Router(); + +router.get('/', async (request, response) => { + response.render('aprilfools'); +}); + +module.exports = router; diff --git a/src/routers/blog.js b/src/routes/blog.js similarity index 78% rename from src/routers/blog.js rename to src/routes/blog.js index cf1528a..2e80238 100644 --- a/src/routers/blog.js +++ b/src/routes/blog.js @@ -1,5 +1,4 @@ const { Router } = require('express'); -const util = require('../util'); const logger = require('../logger'); const router = new Router(); @@ -37,19 +36,9 @@ const postList = () => { router.get('/', async (request, response) => { const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - postList, + postList }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - response.render('blog/blog', renderData); }); @@ -74,18 +63,9 @@ router.get('/:slug', async (request, response, next) => { const renderData = { layout: 'blog-opengraph', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), postList, }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - // Get the name of the post from the URL const postName = request.params.slug; diff --git a/src/routers/docs.js b/src/routes/docs.js similarity index 66% rename from src/routers/docs.js rename to src/routes/docs.js index 56ae3ca..4541988 100644 --- a/src/routers/docs.js +++ b/src/routes/docs.js @@ -1,5 +1,4 @@ const { Router } = require('express'); -const util = require('../util'); const router = new Router(); const fs = require('fs'); @@ -12,44 +11,24 @@ router.get('/', async (request, response) => { router.get('/search', async (request, response) => { const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - currentPage: request.params.slug, + currentPage: request.params.slug }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - response.render('docs/search', renderData); }); router.get('/:slug', async (request, response, next) => { const renderData = { - layout: 'main', - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), - currentPage: request.params.slug, + currentPage: request.params.slug }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - // Get the name of the page from the URL const pageName = request.params.slug; - let markdownLocale = renderData.localeString; + let markdownLocale = response.locals.localeString; let missingInLocale = false; // Check if the MD file exists in the user's locale, if not try en-US and show notice, or finally log error and show 404. - if (fs.existsSync(path.join('docs', renderData.localeString, `${pageName}.md`))) { + if (fs.existsSync(path.join('docs', markdownLocale, `${pageName}.md`))) { null; } else if (fs.existsSync(path.join('docs', 'en-US', `${pageName}.md`))) { markdownLocale = 'en-US'; diff --git a/src/routers/faq.js b/src/routes/faq.js similarity index 100% rename from src/routers/faq.js rename to src/routes/faq.js diff --git a/src/routers/home.js b/src/routes/home.js similarity index 83% rename from src/routers/home.js rename to src/routes/home.js index f2f61e0..9170877 100644 --- a/src/routers/home.js +++ b/src/routes/home.js @@ -1,5 +1,4 @@ const { Router } = require('express'); -const util = require('../util'); const { boards } = require('../../boards/boards.json'); const router = new Router(); @@ -8,25 +7,15 @@ const { getTrelloCache } = require('../cache'); router.get('/', async (request, response) => { const renderData = { - layout: 'main', - boards, - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), + boards }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - const cache = await getTrelloCache(); // Builds the arrays of people for the special thanks section // Shuffles the special thanks people - const specialThanksPeople = renderData.locale.specialThanks.people.slice(); + const specialThanksPeople = response.locals.locale.specialThanks.people.slice(); function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); diff --git a/src/routes/localization.js b/src/routes/localization.js new file mode 100644 index 0000000..6706f4c --- /dev/null +++ b/src/routes/localization.js @@ -0,0 +1,8 @@ +const { Router } = require('express'); +const router = new Router(); + +router.get('/', async (request, response) => { + response.render('localization'); +}); + +module.exports = router; diff --git a/src/routers/progress.js b/src/routes/progress.js similarity index 56% rename from src/routers/progress.js rename to src/routes/progress.js index 4849321..fb133f4 100644 --- a/src/routers/progress.js +++ b/src/routes/progress.js @@ -1,5 +1,4 @@ const { Router } = require('express'); -const util = require('../util'); const { boards } = require('../../boards/boards.json'); const router = new Router(); @@ -7,19 +6,9 @@ const { getTrelloCache, getStripeDonationCache } = require('../cache'); router.get('/', async (request, response) => { const renderData = { - layout: 'main', - boards, - locale: util.getLocale(request.locale.region, request.locale.language), - localeString: request.locale.toString(), + boards }; - renderData.isLoggedIn = request.cookies.access_token && request.cookies.refresh_token && request.cookies.ph; - - if (renderData.isLoggedIn) { - const account = await util.getAccount(request, response); - renderData.account = account; - } - const trelloCache = await getTrelloCache(); renderData.progressLists = trelloCache; const stripeDonationCache = await getStripeDonationCache(); diff --git a/src/schema/pnid.js b/src/schema/pnid.js index a488521..63a2c47 100644 --- a/src/schema/pnid.js +++ b/src/schema/pnid.js @@ -9,6 +9,9 @@ const PNIDSchema = new Schema({ server_access_level: String, access_level: Number, connections: { + discord: { + id: String + }, stripe: { customer_id: String, subscription_id: String, diff --git a/src/server.js b/src/server.js index 4e42444..5f0b094 100644 --- a/src/server.js +++ b/src/server.js @@ -6,9 +6,11 @@ const morgan = require('morgan'); const expressLocale = require('express-locale'); const cookieParser = require('cookie-parser'); const Stripe = require('stripe'); -const logger = require('./logger'); +const redirectMiddleware = require('./middleware/redirect'); +const renderDataMiddleware = require('./middleware/render-data'); const database = require('./database'); const util = require('./util'); +const logger = require('./logger'); const config = require('../config.json'); const defaultLocale = require('../locales/US_en.json'); @@ -18,26 +20,11 @@ const stripe = new Stripe(config.stripe.secret_key); logger.info('Setting up Middleware'); app.use(morgan('dev')); -app.use(express.urlencoded({ extended: true })); - -logger.info('Setting up static public folder'); -app.use(express.static('public')); - -logger.info('Importing page routers'); -const routers = { - home: require('./routers/home'), - faq: require('./routers/faq'), - docs: require('./routers/docs'), - progress: require('./routers/progress'), - account: require('./routers/account'), - blog: require('./routers/blog'), - localization: require('./routers/localization'), - aprilfools: require('./routers/aprilfools') -}; - +app.use(express.json()); +app.use(express.urlencoded({ + extended: true +})); app.use(cookieParser()); - -// Locale express middleware setup app.use(expressLocale({ 'priority': ['cookie', 'accept-language', 'map', 'default'], cookie: { name: 'preferredLocale' }, @@ -82,15 +69,32 @@ app.use(expressLocale({ ], 'default': 'en-US' })); +app.use(redirectMiddleware); +app.use(renderDataMiddleware); -app.use('/', routers.home); -app.use('/faq', routers.faq); -app.use('/docs', routers.docs); -app.use('/progress', routers.progress); -app.use('/account', routers.account); -app.use('/localization', routers.localization); -app.use('/blog', routers.blog); -app.use('/nso-legacy-pack', routers.aprilfools); +logger.info('Setting up static public folder'); +app.use(express.static('public')); + +logger.info('Importing routes'); +const routes = { + home: require('./routes/home'), + faq: require('./routes/faq'), + docs: require('./routes/docs'), + progress: require('./routes/progress'), + account: require('./routes/account'), + blog: require('./routes/blog'), + localization: require('./routes/localization'), + aprilfools: require('./routes/aprilfools') +}; + +app.use('/', routes.home); +app.use('/faq', routes.faq); +app.use('/docs', routes.docs); +app.use('/progress', routes.progress); +app.use('/account', routes.account); +app.use('/localization', routes.localization); +app.use('/blog', routes.blog); +app.use('/nso-legacy-pack', routes.aprilfools); logger.info('Creating 404 status handler'); // This works because it is the last router created diff --git a/src/util.js b/src/util.js index 0394c6c..b542f74 100644 --- a/src/util.js +++ b/src/util.js @@ -59,6 +59,86 @@ function apiDeleteGetRequest(path, headers, json) { }); } +async function register(registerData) { + const apiResponse = await apiPostGetRequest('/v1/register', {}, registerData); + + if (apiResponse.statusCode !== 200) { + throw new Error(apiResponse.body.error); + } + + return apiResponse.body; +} + +async function login(username, password) { + const apiResponse = await apiPostGetRequest('/v1/login', {}, { + username, + password, + grant_type: 'password' + }); + + if (apiResponse.statusCode !== 200) { + throw new Error(apiResponse.body.error); + } + + return apiResponse.body; +} + +async function refreshLogin(request, response) { + const apiResponse = await apiPostGetRequest('/v1/login', {}, { + refresh_token: request.cookies.refresh_token, + grant_type: 'refresh_token' + }); + + if (apiResponse.statusCode !== 200) { + // TODO: Error message + throw new 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' }); +} + +async function getUserAccountData(request, response, fromRetry=false) { + const apiResponse = await apiGetRequest('/v1/user', { + 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}` + }); + + if (apiResponse.statusCode !== 200 && fromRetry === true) { + // TODO: Error message + throw new Error('Bad'); + } + + if (apiResponse.statusCode !== 200) { + await refreshLogin(request, response); + return await getUserAccountData(request, response, true); + } + + return apiResponse.body; +} + +async function updateDiscordConnection(discordUser, request, response, fromRetry=false) { + const apiResponse = await apiPostGetRequest('/v1/connections/add/discord', { + 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}` + }, { + data: { + id: discordUser.id + } + }); + + if (apiResponse.statusCode !== 200 && fromRetry === true) { + // TODO: Error message + throw new Error('Bad'); + } + + if (apiResponse.statusCode !== 200) { + await refreshLogin(request, response); + await updateDiscordConnection(discordUser, request, response, true); + } +} + function nintendoPasswordHash(password, pid) { const pidBuffer = Buffer.alloc(4); pidBuffer.writeUInt32LE(pid); @@ -255,53 +335,17 @@ async function handleStripeEvent(event) { } } -async function getAccount(request, response) { - // Attempt to get user data - let apiResponse = await apiGetRequest('/v1/user', { - 'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}` - }); - - if (apiResponse.statusCode !== 200) { - // Assume expired, refresh and retry request - apiResponse = await 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; - - apiResponse = await 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' - }); - } - - // Return user account info - const account = apiResponse.body; - return account; -} - module.exports = { fullUrl, getLocale, apiGetRequest, apiPostGetRequest, apiDeleteGetRequest, + register, + login, + refreshLogin, + getUserAccountData, + updateDiscordConnection, nintendoPasswordHash, - handleStripeEvent, - getAccount + handleStripeEvent }; \ No newline at end of file diff --git a/views/account/account.handlebars b/views/account/account.handlebars index 35d05ab..70533a7 100644 --- a/views/account/account.handlebars +++ b/views/account/account.handlebars @@ -139,7 +139,7 @@
Connected to Discord as {{ discordUser.username }}#{{ discordUser.discriminator }}
{{else}} -To link your Discord account, run /whatever in the Discord server
+No Discord account linked. Link Discord account here
{{/if}} @@ -156,20 +156,32 @@ -{{#if success}} +{{#if success_message}} {{/if}} -{{#if error}} +{{#if error_message}} {{/if}} + + \ No newline at end of file diff --git a/views/account/upgrade.handlebars b/views/account/upgrade.handlebars index 4c5b2fd..9cca93a 100644 --- a/views/account/upgrade.handlebars +++ b/views/account/upgrade.handlebars @@ -1,6 +1,5 @@