mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-09-29 12:06:40 -05:00
chore: move old codebase into a directory
This commit is contained in:
244
src/cache.js
244
src/cache.js
@@ -1,244 +0,0 @@
|
||||
const { GraphQLClient, gql } = require('graphql-request');
|
||||
const Stripe = require('stripe');
|
||||
const config = require('./config');
|
||||
const logger = require('./logger');
|
||||
|
||||
const github = new GraphQLClient('https://api.github.com/graphql', {
|
||||
headers: {
|
||||
Authorization: `bearer ${config.github.graphql_token}`
|
||||
}
|
||||
});
|
||||
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
|
||||
const getProjectsV2GQL = gql`
|
||||
query getProjectsV2($orgName: String!, $cursor: String!) {
|
||||
organization(login: $orgName) {
|
||||
projectsV2(first: 10, after: $cursor) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
repositories(first: 1) {
|
||||
nodes {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getProjectsV2FieldsGQL = gql`
|
||||
query getProjectsV2Fields($id: ID!, $cursor: String!) {
|
||||
node(id: $id) {
|
||||
... on ProjectV2 {
|
||||
items(first: 10, after: $cursor) {
|
||||
nodes {
|
||||
content {
|
||||
... on DraftIssue {
|
||||
title
|
||||
}
|
||||
... on Issue {
|
||||
title
|
||||
}
|
||||
}
|
||||
fieldValues(first: 20) {
|
||||
nodes {
|
||||
... on ProjectV2ItemFieldSingleSelectValue {
|
||||
name
|
||||
field {
|
||||
... on ProjectV2SingleSelectField {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
let githubProjectsCache = {
|
||||
update_time: 0,
|
||||
sections: []
|
||||
};
|
||||
|
||||
let githubCacheBeingFetched = false;
|
||||
|
||||
let stripeDonationCache = {
|
||||
update_time: 0,
|
||||
sections: []
|
||||
};
|
||||
|
||||
async function getGitHubProjectsV2(after = '') {
|
||||
let projects = [];
|
||||
|
||||
const data = await github.request(getProjectsV2GQL, {
|
||||
orgName: 'PretendoNetwork',
|
||||
cursor: after
|
||||
});
|
||||
|
||||
for (const node of data.organization.projectsV2.nodes) {
|
||||
projects.push({
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
url: node.repositories.nodes[0]?.url
|
||||
});
|
||||
}
|
||||
|
||||
const { hasNextPage, endCursor } = data.organization.projectsV2.pageInfo;
|
||||
|
||||
if (hasNextPage) {
|
||||
const nextPage = await getGitHubProjectsV2(endCursor);
|
||||
projects = [...projects, ...nextPage];
|
||||
}
|
||||
|
||||
return projects;
|
||||
}
|
||||
|
||||
async function getGitHubProjectsV2Fields(id, after = '') {
|
||||
let fields = [];
|
||||
|
||||
const data = await github.request(getProjectsV2FieldsGQL, {
|
||||
id: id,
|
||||
cursor: after
|
||||
});
|
||||
|
||||
for (const node of data.node.items.nodes) {
|
||||
fields.push({
|
||||
title: node.content.title,
|
||||
column: node.fieldValues.nodes.find(fieldValue => fieldValue.field?.name === 'Status')?.name
|
||||
});
|
||||
}
|
||||
|
||||
const { hasNextPage, endCursor } = data.node.items.pageInfo;
|
||||
|
||||
if (hasNextPage) {
|
||||
const nextPage = await getGitHubProjectsV2Fields(id, endCursor);
|
||||
fields = [...fields, ...nextPage];
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
async function getGithubProjectsCache() {
|
||||
if (githubCacheBeingFetched) {
|
||||
return githubProjectsCache;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!githubCacheBeingFetched && githubProjectsCache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
githubCacheBeingFetched = true;
|
||||
githubProjectsCache = await updateGithubProjectsCache();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
} finally {
|
||||
githubCacheBeingFetched = false;
|
||||
}
|
||||
|
||||
return githubProjectsCache;
|
||||
}
|
||||
|
||||
async function updateGithubProjectsCache() {
|
||||
const projectsCacheData = {
|
||||
update_time: Date.now(),
|
||||
sections: []
|
||||
};
|
||||
|
||||
const projects = await getGitHubProjectsV2();
|
||||
|
||||
for (const project of projects) {
|
||||
if (!project.url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const extractedData = {
|
||||
title: project.title,
|
||||
url: project.url,
|
||||
cards: {
|
||||
done: [],
|
||||
in_progress: [],
|
||||
todo: []
|
||||
}
|
||||
};
|
||||
|
||||
const fields = await getGitHubProjectsV2Fields(project.id);
|
||||
|
||||
for (const field of fields) {
|
||||
extractedData.cards[field.column.toLowerCase().replace(' ', '_')]?.push(field.title);
|
||||
}
|
||||
|
||||
projectsCacheData.sections.push(extractedData);
|
||||
}
|
||||
|
||||
return projectsCacheData;
|
||||
}
|
||||
|
||||
async function getStripeDonationCache() {
|
||||
if (!stripeDonationCache) {
|
||||
stripeDonationCache = await updateStripeDonationCache();
|
||||
}
|
||||
|
||||
if (stripeDonationCache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
stripeDonationCache = await updateStripeDonationCache();
|
||||
}
|
||||
|
||||
return stripeDonationCache;
|
||||
}
|
||||
|
||||
async function updateStripeDonationCache() {
|
||||
const donationCache = {
|
||||
update_time: Date.now(),
|
||||
goal: config.stripe.goal_cents,
|
||||
total: 0,
|
||||
donators: 0,
|
||||
completed_real: 0,
|
||||
completed_capped: 0
|
||||
};
|
||||
|
||||
let hasMore = true;
|
||||
let lastId;
|
||||
|
||||
while (hasMore) {
|
||||
const { data: activeSubscriptions, has_more } = await stripe.subscriptions.list({
|
||||
limit: 100,
|
||||
status: 'active',
|
||||
starting_after: lastId
|
||||
});
|
||||
|
||||
donationCache.donators += activeSubscriptions.length;
|
||||
|
||||
for (const subscription of activeSubscriptions) {
|
||||
donationCache.total += subscription.plan.amount;
|
||||
lastId = subscription.id;
|
||||
}
|
||||
|
||||
hasMore = has_more;
|
||||
}
|
||||
|
||||
donationCache.goal_dollars = donationCache.goal / 100;
|
||||
donationCache.total_dollars = donationCache.total / 100;
|
||||
|
||||
donationCache.completed_real = Math.floor((donationCache.total / donationCache.goal) * 100); // real completion amount
|
||||
donationCache.completed_capped = Math.max(0, Math.min(donationCache.completed_real, 100)); // capped at 100
|
||||
|
||||
return donationCache;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGithubProjectsCache,
|
||||
getStripeDonationCache
|
||||
};
|
||||
@@ -1,54 +0,0 @@
|
||||
const jsonConfig = require('../config.json');
|
||||
|
||||
const disableEmail = !jsonConfig.email?.from?.trim() || !jsonConfig.email?.ses?.region?.trim() || !jsonConfig.email?.ses?.key?.trim() || !jsonConfig.email?.ses?.secret?.trim();
|
||||
|
||||
const email = disableEmail
|
||||
? false
|
||||
: {
|
||||
from: jsonConfig.email.from,
|
||||
ses: {
|
||||
region: jsonConfig.email.ses.region,
|
||||
key: jsonConfig.email.ses.key,
|
||||
secret: jsonConfig.email.ses.secret
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
api_base: jsonConfig.api_base,
|
||||
http: {
|
||||
base_url: jsonConfig.http.base_url,
|
||||
port: jsonConfig.http.port,
|
||||
allowed_redirection_suffixes: Array.isArray(jsonConfig.http.allowed_redirection_suffixes) ? jsonConfig.http.allowed_redirection_suffixes : ['pretendo.network']
|
||||
},
|
||||
github: {
|
||||
graphql_token: jsonConfig.github.graphql_token
|
||||
},
|
||||
stripe: {
|
||||
secret_key: jsonConfig.stripe.secret_key,
|
||||
webhook_secret: jsonConfig.stripe.webhook_secret,
|
||||
goal_cents: jsonConfig.stripe.goal_cents,
|
||||
notification_emails: jsonConfig.stripe.notification_emails
|
||||
},
|
||||
database: {
|
||||
account: {
|
||||
connection_string: jsonConfig.database.account.connection_string,
|
||||
options: jsonConfig.database.account.options
|
||||
}
|
||||
},
|
||||
discord: {
|
||||
bot_token: jsonConfig.discord.bot_token,
|
||||
client_id: jsonConfig.discord.client_id,
|
||||
client_secret: jsonConfig.discord.client_secret,
|
||||
guild_id: jsonConfig.discord.guild_id,
|
||||
roles: {
|
||||
supporter: jsonConfig.discord.roles.supporter,
|
||||
tester: jsonConfig.discord.roles.tester
|
||||
}
|
||||
},
|
||||
discourse: {
|
||||
sso: {
|
||||
secret: jsonConfig.discourse.sso.secret
|
||||
}
|
||||
},
|
||||
email
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
const mongoose = require('mongoose');
|
||||
const config = require('./config');
|
||||
const PNIDSchema = require('./schema/pnid');
|
||||
|
||||
const accountServerConfig = config.database.account;
|
||||
const { connection_string, options } = accountServerConfig;
|
||||
let accountServerDBConnection;
|
||||
let PNID;
|
||||
|
||||
async function connect() {
|
||||
accountServerDBConnection = await mongoose.createConnection(connection_string, options);
|
||||
accountServerDBConnection.on('error', console.error.bind(console, 'Mongoose connection error:'));
|
||||
accountServerDBConnection.on('close', () => {
|
||||
accountServerDBConnection.removeAllListeners();
|
||||
});
|
||||
|
||||
await accountServerDBConnection.asPromise();
|
||||
|
||||
PNID = accountServerDBConnection.model('PNID', PNIDSchema);
|
||||
|
||||
module.exports.PNID = PNID;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
connect,
|
||||
PNID
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"face": [0, 1, 8, 2, 3, 9, 4, 5, 10, 6, 7, 11],
|
||||
"hairs": [
|
||||
[33, 47, 40, 37, 32, 107, 48, 51, 55, 70, 44, 66],
|
||||
[52, 50, 38, 49, 43, 31, 56, 68, 62, 115, 76, 119],
|
||||
[64, 81, 116, 121, 22, 58, 60, 87, 125, 117, 73, 75],
|
||||
[42, 89, 57, 54, 80, 34, 23, 86, 88, 118, 39, 36],
|
||||
[45, 67, 59, 65, 41, 30, 12, 16, 10, 82, 128, 129],
|
||||
[14, 95, 105, 100, 6, 20, 93, 102, 27, 4, 17, 110],
|
||||
[123, 8, 106, 72, 3, 21, 0, 98, 63, 90, 11, 120],
|
||||
[5, 74, 108, 94, 124, 25, 99, 69, 35, 13, 122, 113],
|
||||
[53, 24, 85, 83, 71, 131, 96, 101, 29, 7, 15, 112],
|
||||
[79, 1, 109, 127, 91, 26, 61, 103, 2, 77, 18, 92],
|
||||
[84, 9, 19, 130, 97, 104, 46, 78, 28, 114, 126, 111]
|
||||
],
|
||||
"eyebrows": [
|
||||
[6, 0, 12, 1, 9, 19, 7, 21, 8, 17, 5, 4],
|
||||
[11, 10, 2, 3, 14, 20, 15, 13, 22, 18, 16, 23]
|
||||
],
|
||||
"eyes": [
|
||||
[2, 4, 0, 8, 39, 17, 1, 26, 16, 15, 27, 20],
|
||||
[33, 11, 19, 32, 9, 12, 23, 34, 21, 25, 40, 35],
|
||||
[5, 41, 13, 36, 37, 6, 24, 30, 31, 18, 28, 46],
|
||||
[7, 44, 38, 42, 45, 29, 3, 43, 22, 10, 14, 47],
|
||||
[48, 49, 50, 53, 59, 56, 54, 58, 57, 55, 51, 52]
|
||||
],
|
||||
"nose": [
|
||||
[1, 10, 2, 3, 6, 0, 5, 4, 8, 9, 7, 11],
|
||||
[13, 14, 12, 17, 16, 15]
|
||||
],
|
||||
"mouth": [
|
||||
[23, 1, 19, 21, 22, 5, 0, 8, 10, 16, 6, 13],
|
||||
[7, 9, 2, 17, 3, 4, 15, 11, 20, 18, 14, 12],
|
||||
[27, 30, 24, 25, 29, 28, 26, 35, 31, 34, 33, 32]
|
||||
],
|
||||
"arrayOf6": [0, 1, 2, 3, 4, 5],
|
||||
"arrayOf9": [0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
"arrayOf12": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
const fs = require('fs-extra');
|
||||
require('colors');
|
||||
|
||||
const root = __dirname + '/../';
|
||||
fs.ensureDirSync(`${root}/logs`);
|
||||
|
||||
const streams = {
|
||||
latest: fs.createWriteStream(`${root}/logs/latest.log`),
|
||||
success: fs.createWriteStream(`${root}/logs/success.log`),
|
||||
error: fs.createWriteStream(`${root}/logs/error.log`),
|
||||
warn: fs.createWriteStream(`${root}/logs/warn.log`),
|
||||
info: fs.createWriteStream(`${root}/logs/info.log`)
|
||||
};
|
||||
|
||||
function success(input) {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [SUCCESS]: ${input}`;
|
||||
streams.success.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.green.bold);
|
||||
}
|
||||
|
||||
function error(input) {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [ERROR]: ${input}`;
|
||||
streams.error.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.red.bold);
|
||||
}
|
||||
|
||||
function warn(input) {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [WARN]: ${input}`;
|
||||
streams.warn.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.yellow.bold);
|
||||
}
|
||||
|
||||
function info(input) {
|
||||
const time = new Date();
|
||||
input = `[${time.getHours()}:${time.getMinutes()}:${time.getSeconds()}] [INFO]: ${input}`;
|
||||
streams.info.write(`${input}\n`);
|
||||
|
||||
console.log(`${input}`.cyan.bold);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
success,
|
||||
error,
|
||||
warn,
|
||||
info
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const aws = require('@aws-sdk/client-ses');
|
||||
const config = require('./config');
|
||||
|
||||
let transporter;
|
||||
|
||||
if (config.email) {
|
||||
const ses = new aws.SES({
|
||||
apiVersion: '2010-12-01',
|
||||
region: config.email.ses.region,
|
||||
credentials: {
|
||||
accessKeyId: config.email.ses.key,
|
||||
secretAccessKey: config.email.ses.secret
|
||||
}
|
||||
});
|
||||
|
||||
transporter = transporter = nodemailer.createTransport({
|
||||
SES: {
|
||||
ses,
|
||||
aws
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function sendMail(options) {
|
||||
if (transporter) {
|
||||
options.from = config.email.from;
|
||||
|
||||
await transporter.sendMail(options);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMail
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
const config = require('../config');
|
||||
|
||||
function isValidRedirect(redirect) {
|
||||
if (!redirect) {
|
||||
return false;
|
||||
}
|
||||
if (redirect.startsWith('/')) {
|
||||
return true;
|
||||
}
|
||||
if (redirect.startsWith('http://') || redirect.startsWith('https://')) {
|
||||
try {
|
||||
const url = new URL(redirect);
|
||||
return config.http.allowed_redirection_suffixes.some(domain => url.hostname.endsWith(domain));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function redirectMiddleware(request, response, next) {
|
||||
if (request.path.startsWith('/account/logout')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (request.method === 'POST' && request.body) {
|
||||
request.redirect = isValidRedirect(request.body.redirect) ? request.body.redirect : null;
|
||||
}
|
||||
|
||||
if (request.query.redirect) {
|
||||
response.locals.redirect = isValidRedirect(request.query.redirect) ? request.query.redirect : null;
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
module.exports = redirectMiddleware;
|
||||
@@ -1,110 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const util = require('../util');
|
||||
const database = require('../database');
|
||||
const localeFileNames = fs.readdirSync(`${__dirname}/../../locales`);
|
||||
|
||||
async function renderDataMiddleware(request, response, next) {
|
||||
if (request.path.startsWith('/assets')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (request.path.startsWith('/account/logout')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Get user locale
|
||||
const reqLocale = request.cookies.preferredLocale || request.locale.toString();
|
||||
const locale = util.getLocale(reqLocale);
|
||||
|
||||
let localeList = localeFileNames.map((locale) => {
|
||||
const code = locale.replace('.json', '').replace('_', '-');
|
||||
|
||||
// Check if it's a real language code, or a custom one
|
||||
if (!code.includes('@')) {
|
||||
const enDisp = new Intl.DisplayNames([code], {
|
||||
type: 'language',
|
||||
languageDisplay: 'standard'
|
||||
});
|
||||
const languageName = enDisp.of(code);
|
||||
|
||||
return {
|
||||
code,
|
||||
languageName
|
||||
};
|
||||
} else {
|
||||
switch (code) {
|
||||
case 'en@uwu':
|
||||
return {
|
||||
code,
|
||||
languageName: 'English (lolcat)'
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
code,
|
||||
languageName: 'Unknown'
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// sort the array alphabetically by languageName while making sure that objects with language codes starting with 'en' are at the top
|
||||
localeList = localeList.sort((a, b) => {
|
||||
if (a.code.startsWith('en') && !b.code.startsWith('en')) {
|
||||
return -1;
|
||||
} else if (!a.code.startsWith('en') && b.code.startsWith('en')) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.languageName.localeCompare(b.languageName);
|
||||
}
|
||||
});
|
||||
|
||||
// move all the objects with language codes containing '@' to the end of the array
|
||||
localeList = localeList.sort((a, b) => {
|
||||
if (a.code.includes('@') && !b.code.includes('@')) {
|
||||
return 1;
|
||||
} else if (!a.code.includes('@') && b.code.includes('@')) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
response.locals.localeList = localeList;
|
||||
|
||||
response.locals.locale = locale;
|
||||
response.locals.localeString = reqLocale;
|
||||
|
||||
// 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;
|
||||
|
||||
if (request.pnid.deleted) {
|
||||
// TODO - We just need to overhaul our API tbh
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (error) {
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account/logout');
|
||||
}
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = renderDataMiddleware;
|
||||
@@ -1,14 +0,0 @@
|
||||
async function requireLoginMiddleware(request, response, next) {
|
||||
if (request.path.startsWith('/account/logout')) {
|
||||
return 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;
|
||||
@@ -1,569 +0,0 @@
|
||||
const crypto = require('crypto');
|
||||
const express = require('express');
|
||||
const DiscordOauth2 = require('discord-oauth2');
|
||||
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 { handleStripeEvent } = require('../stripe');
|
||||
const logger = require('../logger');
|
||||
const editorJSON = require('../json/miieditor.json');
|
||||
const config = require('../config');
|
||||
|
||||
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.environment = pnid.server_access_level;
|
||||
renderData.tierName = pnid.connections.stripe.tier_name;
|
||||
renderData.tierLevel = pnid.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) => {
|
||||
const renderData = {
|
||||
error: request.cookies.error_message,
|
||||
loginPath: '/account/login'
|
||||
};
|
||||
|
||||
response.render('account/login', renderData);
|
||||
});
|
||||
|
||||
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,
|
||||
error: request.cookies.error_message
|
||||
};
|
||||
|
||||
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('/forgot-password', async (request, response) => {
|
||||
const renderData = {
|
||||
input: request.cookies.input,
|
||||
success_message: request.cookies.success_message,
|
||||
error_message: request.cookies.error_message
|
||||
};
|
||||
|
||||
response.clearCookie('input', { domain: '.pretendo.network' });
|
||||
|
||||
response.render('account/forgot-password', renderData);
|
||||
});
|
||||
|
||||
router.post('/forgot-password', async (request, response) => {
|
||||
const { input, 'h-captcha-response': hCaptchaResponse } = request.body;
|
||||
|
||||
response.cookie('input', input, { domain: '.pretendo.network' });
|
||||
|
||||
try {
|
||||
await util.forgotPassword({
|
||||
input,
|
||||
hCaptchaResponse
|
||||
});
|
||||
|
||||
response.clearCookie('input', { domain: '.pretendo.network' });
|
||||
|
||||
response.cookie('success_message', 'An email has been sent.', { domain: '.pretendo.network' });
|
||||
|
||||
response.redirect(request.redirect || '/account/forgot-password');
|
||||
} catch (error) {
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account/forgot-password');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/reset-password', async (request, response) => {
|
||||
const renderData = {
|
||||
token: decodeURIComponent(request.query.token)
|
||||
};
|
||||
|
||||
response.render('account/reset-password', renderData);
|
||||
});
|
||||
|
||||
router.get('/connect/discord', requireLoginMiddleware, async (request, response) => {
|
||||
const { pnid } = request;
|
||||
let tokens;
|
||||
try {
|
||||
// Attempt to get OAuth2 tokens
|
||||
tokens = await discordOAuth.tokenRequest({
|
||||
code: request.query.code,
|
||||
scope: 'identify guilds',
|
||||
grantType: 'authorization_code'
|
||||
});
|
||||
} catch {
|
||||
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);
|
||||
|
||||
const priceId = pnid.get('connections.stripe.price_id');
|
||||
|
||||
if (priceId && priceId.trim() !== '') {
|
||||
const price = await stripe.prices.retrieve(priceId);
|
||||
const product = await stripe.products.retrieve(price.product);
|
||||
const discordRoleId = product.metadata.discord_role_id;
|
||||
const discordId = discordUser.id;
|
||||
|
||||
await util.assignDiscordMemberSupporterRole(discordId, discordRoleId);
|
||||
|
||||
if (product.metadata.beta === 'true') {
|
||||
await util.assignDiscordMemberTesterRole(discordId);
|
||||
}
|
||||
}
|
||||
|
||||
response.cookie('success_message', 'Discord account linked successfully', { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
} catch (error) {
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/remove/discord', requireLoginMiddleware, async (request, response) => {
|
||||
const { account, pnid } = request;
|
||||
|
||||
try {
|
||||
await util.removeDiscordConnection(request, response);
|
||||
|
||||
const priceId = pnid.get('connections.stripe.price_id');
|
||||
|
||||
if (priceId && priceId.trim() !== '') {
|
||||
const price = await stripe.prices.retrieve(priceId);
|
||||
const product = await stripe.products.retrieve(price.product);
|
||||
const discordRoleId = product.metadata.discord_role_id;
|
||||
const discordId = account.connections.discord.id;
|
||||
|
||||
await util.removeDiscordMemberSupporterRole(discordId, discordRoleId);
|
||||
|
||||
if (product.metadata.beta === 'true') {
|
||||
await util.removeDiscordMemberTesterRole(discordId);
|
||||
}
|
||||
}
|
||||
|
||||
response.cookie('success_message', 'Discord account removed successfully', { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
} catch (error) {
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/miieditor', requireLoginMiddleware, async (request, response) => {
|
||||
const { account } = request;
|
||||
|
||||
response.render('account/miieditor', {
|
||||
encodedUserMiiData: account.mii.data,
|
||||
editorJSON
|
||||
});
|
||||
});
|
||||
|
||||
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.id === product.default_price);
|
||||
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', async (request, response) => {
|
||||
const stripeSignature = request.headers['stripe-signature'];
|
||||
let event;
|
||||
|
||||
try {
|
||||
event = stripe.webhooks.constructEvent(request.rawBody, stripeSignature, config.stripe.webhook_secret);
|
||||
} catch (error) {
|
||||
logger.error(error.message);
|
||||
return response.status(400).send(`Webhook Error: ${error.message}`);
|
||||
}
|
||||
|
||||
await handleStripeEvent(event);
|
||||
|
||||
response.json({ received: true });
|
||||
});
|
||||
|
||||
router.get('/sso/discourse', async (request, response, next) => {
|
||||
if (!request.query.sso || !request.query.sig) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
const signature = util.signDiscoursePayload(request.query.sso);
|
||||
|
||||
if (signature !== request.query.sig) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
const decodedPayload = new URLSearchParams(Buffer.from(request.query.sso, 'base64').toString());
|
||||
|
||||
if (!decodedPayload.has('nonce') || !decodedPayload.has('return_sso_url')) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
// * User already logged in, don't show the login prompt
|
||||
if (request.cookies.access_token && request.cookies.refresh_token) {
|
||||
try {
|
||||
const accountData = await util.getUserAccountData(request, response);
|
||||
|
||||
// * Discourse REQUIRES unique emails, however we do not due to NN also
|
||||
// * not requiring unique email addresses. Email addresses, for now,
|
||||
// * are faked using the users PID. This will essentially disable email
|
||||
// * for the forum, but it's a bullet we have to bite for right now.
|
||||
// TODO - We can run our own SMTP server which maps fake emails (pid@pretendo.whatever) to users real emails
|
||||
const payload = Buffer.from(new URLSearchParams({
|
||||
nonce: decodedPayload.get('nonce'),
|
||||
external_id: accountData.pid,
|
||||
email: `${accountData.pid}@invalid.com`, // * Hack to get unique emails
|
||||
username: accountData.username,
|
||||
name: accountData.username,
|
||||
avatar_url: accountData.mii.image_url,
|
||||
avatar_force_update: true
|
||||
}).toString()).toString('base64');
|
||||
|
||||
const query = new URLSearchParams({
|
||||
sso: payload,
|
||||
sig: util.signDiscoursePayload(payload)
|
||||
}).toString();
|
||||
|
||||
return response.redirect(`${decodedPayload.get('return_sso_url')}?${query}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account/logout');
|
||||
}
|
||||
}
|
||||
|
||||
// * User not logged in already, show the login page
|
||||
const renderData = {
|
||||
discourse: {
|
||||
// * Fast and dirty sanitization. If the strings contain
|
||||
// * characters not allow in their encodings, they are removed
|
||||
// * when doing this decode-encode. Since neither base64/hex
|
||||
// * allow characters such as < and >, this prevents injection.
|
||||
payload: Buffer.from(request.query.sso, 'base64').toString('base64'),
|
||||
signature: Buffer.from(request.query.sig, 'hex').toString('hex')
|
||||
},
|
||||
loginPath: '/account/sso/discourse'
|
||||
};
|
||||
|
||||
response.render('account/login', renderData); // * Just reuse the /account/login page, no need to duplicate the pages
|
||||
});
|
||||
|
||||
router.post('/sso/discourse', async (request, response, next) => {
|
||||
if (!request.body['discourse-sso-payload'] || !request.body['discourse-sso-signature']) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
const { username, password } = request.body;
|
||||
|
||||
// * Fast and dirty sanitization. If the strings contain
|
||||
// * characters not allow in their encodings, they are removed
|
||||
// * when doing this decode-encode. Since neither base64/hex
|
||||
// * allow characters such as < and >, this prevents injection.
|
||||
const discoursePayload = Buffer.from(request.body['discourse-sso-payload'], 'base64').toString('base64');
|
||||
const discourseSignature = Buffer.from(request.body['discourse-sso-signature'], 'hex').toString('hex');
|
||||
|
||||
const signature = util.signDiscoursePayload(discoursePayload);
|
||||
|
||||
if (signature !== discourseSignature) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
const decodedPayload = new URLSearchParams(Buffer.from(discoursePayload, 'base64').toString());
|
||||
|
||||
if (!decodedPayload.has('nonce') || !decodedPayload.has('return_sso_url')) {
|
||||
return next(); // * 404
|
||||
}
|
||||
|
||||
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' });
|
||||
|
||||
// * Need to set these here so that getUserAccountData can see them
|
||||
request.cookies.refresh_token = tokens.refresh_token;
|
||||
request.cookies.access_token = tokens.access_token;
|
||||
request.cookies.token_type = tokens.token_type;
|
||||
|
||||
const accountData = await util.getUserAccountData(request, response);
|
||||
|
||||
// * Discourse REQUIRES unique emails, however we do not due to NN also
|
||||
// * not requiring unique email addresses. Email addresses, for now,
|
||||
// * are faked using the users PID. This will essentially disable email
|
||||
// * for the forum, but it's a bullet we have to bite for right now.
|
||||
// TODO - We can run our own SMTP server which maps fake emails (pid@pretendo.whatever) to users real emails
|
||||
const payload = Buffer.from(new URLSearchParams({
|
||||
nonce: decodedPayload.get('nonce'),
|
||||
external_id: accountData.pid,
|
||||
email: `${accountData.pid}@invalid.com`, // * Hack to get unique emails
|
||||
username: accountData.username,
|
||||
name: accountData.username,
|
||||
avatar_url: accountData.mii.image_url,
|
||||
avatar_force_update: true
|
||||
}).toString()).toString('base64');
|
||||
|
||||
const query = new URLSearchParams({
|
||||
sso: payload,
|
||||
sig: util.signDiscoursePayload(payload)
|
||||
}).toString();
|
||||
|
||||
return response.redirect(`${decodedPayload.get('return_sso_url')}?${query}`);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
response.cookie('error_message', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,8 +0,0 @@
|
||||
const { Router } = require('express');
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
response.render('aprilfools');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,104 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { Router } = require('express');
|
||||
const { marked } = require('marked');
|
||||
const matter = require('gray-matter');
|
||||
const logger = require('../logger');
|
||||
|
||||
const router = new Router();
|
||||
|
||||
async function getPostsList() {
|
||||
const files = await fs.readdir('blogposts');
|
||||
|
||||
const filteredFiles = files
|
||||
.filter(filename => !filename.startsWith('_')) // Ignore files starting with _
|
||||
.filter(filename => filename.endsWith('.md')); // Ignores files that don't end with .md
|
||||
|
||||
const posts = await Promise.all(filteredFiles.map(async (filename) => {
|
||||
const slug = filename.replace('.md', '');
|
||||
const rawPost = await fs.readFile(
|
||||
path.join('blogposts', `${filename}`),
|
||||
'utf-8'
|
||||
);
|
||||
const { data: postInfo } = matter(rawPost);
|
||||
return {
|
||||
slug,
|
||||
postInfo
|
||||
};
|
||||
}));
|
||||
|
||||
posts.sort((a, b) => {
|
||||
return new Date(b.postInfo.date) - new Date(a.postInfo.date);
|
||||
});
|
||||
|
||||
return posts;
|
||||
}
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
const postList = await getPostsList();
|
||||
const renderData = {
|
||||
postList
|
||||
};
|
||||
|
||||
response.render('blog/blog', renderData);
|
||||
});
|
||||
|
||||
// RSS feed
|
||||
router.get('/feed.xml', async (request, response) => {
|
||||
const postList = await getPostsList();
|
||||
|
||||
// Adds the pubDate and the cover_extension to the post array
|
||||
const posts = postList.map((post) => {
|
||||
post.postInfo.pubDate = new Date(post.postInfo.date).toUTCString();
|
||||
post.postInfo.cover_extension = post.postInfo.cover_image.substring(post.postInfo.cover_image.lastIndexOf('.') + 1);
|
||||
return post;
|
||||
});
|
||||
|
||||
response.set('Content-Type', 'application/rss+xml');
|
||||
response.render('blog/blog-rss', {
|
||||
layout: false,
|
||||
posts
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:slug', async (request, response, next) => {
|
||||
const renderData = {
|
||||
layout: 'blog-opengraph'
|
||||
};
|
||||
|
||||
// Get the name of the post from the URL
|
||||
const postName = request.params.slug;
|
||||
|
||||
if (!/^[0-9-]+$/.test(postName)) {
|
||||
logger.error(`Invalid blog post name name ${postName}`);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the markdown file corresponding to the post
|
||||
let rawPost;
|
||||
try {
|
||||
rawPost = await fs.readFile(path.join('blogposts', `${postName}.md`), 'utf-8');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
// Convert the post info into JSON and separate it and the content
|
||||
|
||||
let { data: postInfo, content } = matter(rawPost);
|
||||
renderData.postInfo = postInfo;
|
||||
|
||||
// Replace [yt-iframe](videoID) with the full <iframe />
|
||||
content = content
|
||||
.replace(/(?<!`)\[yt-iframe]\(/g, '<div class="aspectratio-fallback"><iframe src="https://www.youtube-nocookie.com/embed/')
|
||||
.replace(/(?<=<iframe src="https:\/\/www\.youtube-nocookie\.com\/embed\/.{11})\)/g, '" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>');
|
||||
|
||||
// Convert the content into HTML
|
||||
const htmlPost = marked.parse(content);
|
||||
renderData.htmlPost = htmlPost;
|
||||
|
||||
response.render('blog/blogpost', renderData);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,124 +0,0 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
const { Router } = require('express');
|
||||
const errors = require('@pretendonetwork/error-codes');
|
||||
const util = require('../util');
|
||||
|
||||
const errorList = errors.getAllErrors();
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
response.redirect('/docs/welcome');
|
||||
});
|
||||
|
||||
router.get('/welcome', async (request, response) => {
|
||||
const renderData = {
|
||||
currentPage: 'welcome'
|
||||
};
|
||||
|
||||
response.render('docs/welcome', renderData);
|
||||
});
|
||||
|
||||
router.get('/install', async (request, response) => {
|
||||
const renderData = {
|
||||
currentPage: 'install'
|
||||
};
|
||||
|
||||
response.render('docs/install', renderData);
|
||||
});
|
||||
|
||||
router.get([
|
||||
'/search', // TODO - Deprecate search page
|
||||
'/errors'
|
||||
], async (_request, response) => {
|
||||
const renderData = {
|
||||
errorList: JSON.stringify(errorList),
|
||||
currentPage: 'errors'
|
||||
};
|
||||
|
||||
response.render('docs/search', renderData);
|
||||
});
|
||||
|
||||
router.get('/error', async (_request, response) => {
|
||||
response.redirect(307, '/docs/errors');
|
||||
});
|
||||
|
||||
router.get('/:page', async (request, response, next) => {
|
||||
const renderData = {};
|
||||
|
||||
const locale = response.locals.localeString;
|
||||
const pageName = request.params.page;
|
||||
renderData.currentPage = pageName;
|
||||
|
||||
const { content, MDLocale } = util.getRawDocs(locale, '', pageName);
|
||||
if (content) {
|
||||
renderData.content = content;
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
renderData.missingInLocale = locale !== MDLocale;
|
||||
|
||||
response.render('docs/docs', renderData);
|
||||
});
|
||||
|
||||
router.get([
|
||||
'/errors/:errorCode', // TODO - Deprecate "errors" route
|
||||
'/error/:errorCode'
|
||||
], async (request, response, next) => {
|
||||
const locale = response.locals.localeString;
|
||||
const error = request.params.errorCode;
|
||||
|
||||
const [sysmodule, errorCode] = error.split('-');
|
||||
|
||||
if (!errorCode) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let template = fs.readFileSync(path.join(__dirname, '../../docs/common/error-page-template.md'), {
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
const errorInfo = errors.getErrorInfo(sysmodule, errorCode, locale);
|
||||
|
||||
if (!errorInfo) {
|
||||
return next();
|
||||
}
|
||||
|
||||
template = template.replace('{module}', sysmodule);
|
||||
template = template.replace('{code}', errorCode);
|
||||
template = template.replace('{system}', errorInfo.module.system);
|
||||
template = template.replace('{module_name}', errorInfo.module.name);
|
||||
template = template.replace('{module_description}', errorInfo.module.description);
|
||||
template = template.replace('{message}', errorInfo.message.replace(/\s\s+/g, ' '));
|
||||
template = template.replace('{description}', errorInfo.long_description);
|
||||
template = template.replace('{solution}', errorInfo.long_solution);
|
||||
|
||||
response.render('docs/docs', {
|
||||
content: util.parseDocs(template),
|
||||
currentPage: 'errors'
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/:subpath/:page', async (request, response, next) => {
|
||||
const locale = response.locals.localeString;
|
||||
const pageName = request.params.page;
|
||||
const subpath = request.params.subpath;
|
||||
|
||||
const renderData = {
|
||||
currentPage: `${subpath}/${pageName}`
|
||||
};
|
||||
|
||||
const { content, MDLocale } = util.getRawDocs(locale, subpath, pageName);
|
||||
|
||||
if (content) {
|
||||
renderData.content = content;
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
|
||||
renderData.missingInLocale = locale !== MDLocale;
|
||||
|
||||
response.render('docs/docs', renderData);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,8 +0,0 @@
|
||||
const { Router } = require('express');
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', (request, response) => {
|
||||
response.redirect('/#faq');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,80 +0,0 @@
|
||||
const { Router } = require('express');
|
||||
const { getGithubProjectsCache } = require('../cache');
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
const renderData = {};
|
||||
|
||||
const githubProjectsCache = await getGithubProjectsCache();
|
||||
|
||||
// Builds the arrays of people for the special thanks section
|
||||
|
||||
// Shuffles the special thanks people
|
||||
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));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
shuffleArray(specialThanksPeople);
|
||||
|
||||
const pivot = specialThanksPeople.length / 2;
|
||||
// Slices the array in half
|
||||
const specialThanksFirstRow = specialThanksPeople.slice(0, pivot);
|
||||
const specialThanksSecondRow = specialThanksPeople.slice(pivot);
|
||||
|
||||
// Builds the final array to be sent to the view, and triples each row.
|
||||
renderData.specialThanksPeople = {
|
||||
first: specialThanksFirstRow.concat(specialThanksFirstRow).concat(specialThanksFirstRow),
|
||||
second: specialThanksSecondRow.concat(specialThanksSecondRow).concat(specialThanksSecondRow)
|
||||
};
|
||||
|
||||
// Progress
|
||||
|
||||
// Creates an object to hold the total progress
|
||||
const totalProgress = {
|
||||
title: null,
|
||||
_calc: {
|
||||
percentageSum: 0
|
||||
},
|
||||
percent: 0,
|
||||
cards: {
|
||||
todo: [],
|
||||
in_progress: [],
|
||||
done: []
|
||||
}
|
||||
};
|
||||
|
||||
// Calculates individual completion percentages and progress states
|
||||
githubProjectsCache.sections.forEach((section) => {
|
||||
const { todo, in_progress, done } = section.cards;
|
||||
|
||||
// Calculates the completion percentage of the project, and sums it to the total
|
||||
const sectionCompletionPercentage = ((done.length + in_progress.length * 0.5) / (done.length + in_progress.length + todo.length)) || 0;
|
||||
totalProgress._calc.percentageSum += sectionCompletionPercentage;
|
||||
|
||||
const sectionTitle = `${section.title} [${Math.floor(sectionCompletionPercentage * 100)}%]`;
|
||||
|
||||
switch (sectionCompletionPercentage) {
|
||||
case 0:
|
||||
totalProgress.cards.todo.push(sectionTitle);
|
||||
break;
|
||||
case 1:
|
||||
totalProgress.cards.done.push(sectionTitle);
|
||||
break;
|
||||
default:
|
||||
totalProgress.cards.in_progress.push(sectionTitle);
|
||||
}
|
||||
});
|
||||
|
||||
// Calculates global completion percentage
|
||||
totalProgress.percent = Math.round(totalProgress._calc.percentageSum / githubProjectsCache.sections.length * 100);
|
||||
|
||||
renderData.featuredFeatureList = totalProgress;
|
||||
|
||||
response.render('home', renderData);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,8 +0,0 @@
|
||||
const { Router } = require('express');
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
response.redirect('https://hosted.weblate.org/projects/pretendonetwork/');
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,15 +0,0 @@
|
||||
const { Router } = require('express');
|
||||
const { getGithubProjectsCache, getStripeDonationCache } = require('../cache');
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
const renderData = {
|
||||
progressLists: await getGithubProjectsCache(),
|
||||
donationCache: await getStripeDonationCache()
|
||||
};
|
||||
|
||||
response.render('progress', renderData);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,41 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const { Router } = require('express');
|
||||
const { marked } = require('marked');
|
||||
const matter = require('gray-matter');
|
||||
const logger = require('../logger');
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.get('/:slug', async (request, response, next) => {
|
||||
const renderData = {
|
||||
layout: 'blog-opengraph'
|
||||
};
|
||||
|
||||
const termName = request.params.slug;
|
||||
|
||||
if (!/^[a-z]+$/.test(termName)) {
|
||||
logger.error(`Invalid term name ${termName}`);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
let rawTerm;
|
||||
try {
|
||||
rawTerm = await fs.readFile(path.join('terms', `${termName}.md`), 'utf-8');
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: termInfo, content } = matter(rawTerm);
|
||||
renderData.termInfo = termInfo;
|
||||
|
||||
const htmlPost = marked.parse(content);
|
||||
renderData.htmlPost = htmlPost;
|
||||
|
||||
response.render('terms/term', renderData);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,28 +0,0 @@
|
||||
const { Schema } = require('mongoose');
|
||||
|
||||
// Only define what we will be using
|
||||
const PNIDSchema = new Schema({
|
||||
deleted: Boolean,
|
||||
pid: {
|
||||
type: Number,
|
||||
unique: true
|
||||
},
|
||||
server_access_level: String,
|
||||
access_level: Number,
|
||||
username: String,
|
||||
connections: {
|
||||
discord: {
|
||||
id: String
|
||||
},
|
||||
stripe: {
|
||||
customer_id: String,
|
||||
subscription_id: String,
|
||||
price_id: String,
|
||||
tier_level: Number,
|
||||
tier_name: String,
|
||||
latest_webhook_timestamp: Number
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = PNIDSchema;
|
||||
194
src/server.js
194
src/server.js
@@ -1,194 +0,0 @@
|
||||
process.title = 'Pretendo - Website';
|
||||
process.on('SIGTERM', () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
const express = require('express');
|
||||
const handlebars = require('express-handlebars');
|
||||
const morgan = require('morgan');
|
||||
const expressLocale = require('express-locale');
|
||||
const cookieParser = require('cookie-parser');
|
||||
// const Stripe = require('stripe');
|
||||
const config = require('./config');
|
||||
const redirectMiddleware = require('./middleware/redirect');
|
||||
const renderDataMiddleware = require('./middleware/render-data');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
|
||||
const { http: { port } } = config;
|
||||
const app = express();
|
||||
// const stripe = new Stripe(config.stripe.secret_key);
|
||||
|
||||
logger.info('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
// app.use(express.json());
|
||||
app.use(express.json({
|
||||
verify: (req, res, buf) => {
|
||||
req.rawBody = buf;
|
||||
}
|
||||
}));
|
||||
app.use(express.urlencoded({
|
||||
extended: true
|
||||
}));
|
||||
app.use(cookieParser());
|
||||
app.use(expressLocale({
|
||||
priority: ['cookie', 'accept-language', 'map', 'default'],
|
||||
cookie: { name: 'preferredLocale' },
|
||||
// Map unavailable regions to available locales from the same language
|
||||
map: {
|
||||
/* TODO: map more regions to the available locales */
|
||||
'en': 'en-US', 'en-AU': 'en-US', 'en-CA': 'en-US',
|
||||
'ar': 'ar-AR',
|
||||
'ca': 'ca-ES',
|
||||
'cs': 'cs-CZ',
|
||||
'cn': 'zh-CN',
|
||||
'de': 'de-DE',
|
||||
'nl': 'nl-NL',
|
||||
'es': 'es-ES',
|
||||
'fr': 'fr-FR', 'fr-CA': 'fr-FR', 'fr-CH': 'fr-FR',
|
||||
'fi': 'fi-FI',
|
||||
'it': 'it-IT', 'it-CH': 'it-IT',
|
||||
'ja': 'ja-JP',
|
||||
'kk': 'kk-KZ',
|
||||
'ko': 'ko-KR',
|
||||
'nb': 'nb-NO',
|
||||
'no': 'nb-NO',
|
||||
'pl': 'pl-PL',
|
||||
'pt': 'pt-BR',
|
||||
'ro': 'ro-RO',
|
||||
'ru': 'ru-RU',
|
||||
'sr': 'sr-RS',
|
||||
'tr': 'tr-TR',
|
||||
'uk': 'uk-UA'
|
||||
},
|
||||
allowed: [
|
||||
'en', 'en-US', 'en-GB', 'en-AU', 'en-CA',
|
||||
'ar', 'ar-AR',
|
||||
'ast',
|
||||
'ca-ES',
|
||||
'cs-CZ',
|
||||
'cn', 'zh-CN', 'zh-HK', 'zh-TW',
|
||||
'de', 'de-DE',
|
||||
'nl', 'nl-NL',
|
||||
'es', 'es-ES',
|
||||
'fi', 'fi-FI',
|
||||
'fr', 'fr-FR', 'fr-CA', 'fr-CH',
|
||||
'it', 'it-IT', 'it-CH',
|
||||
'ja', 'ja-JP',
|
||||
'kk', 'kk-KZ',
|
||||
'ko', 'ko-KR',
|
||||
'nb', 'no', 'nb-NO',
|
||||
'pl', 'pl-PL',
|
||||
'pt', 'pt-BR',
|
||||
'ro', 'ro-RO',
|
||||
'ru', 'ru-RU',
|
||||
'sr', 'sr-RS',
|
||||
'tr', 'tr-TR',
|
||||
'uk', 'uk-UA',
|
||||
'en@uwu'
|
||||
],
|
||||
default: 'en-US'
|
||||
}));
|
||||
app.use(redirectMiddleware);
|
||||
app.use(renderDataMiddleware);
|
||||
|
||||
logger.info('Setting up static public folder');
|
||||
app.use(express.static('public'));
|
||||
|
||||
logger.info('Importing routes');
|
||||
const routes = {
|
||||
home: require('./routes/home'),
|
||||
terms: require('./routes/terms'),
|
||||
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('/terms', routes.terms);
|
||||
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
|
||||
// Meaning the request could not find a valid router
|
||||
app.use((request, response) => {
|
||||
response.render('404');
|
||||
});
|
||||
|
||||
logger.info('Setting up handlebars engine');
|
||||
app.engine('handlebars', handlebars({
|
||||
helpers: {
|
||||
doFaq(value, options) {
|
||||
let htmlLeft = '';
|
||||
let htmlRight = '';
|
||||
for (const [i, v] of Object.entries(value)) {
|
||||
const appendHtml = options.fn({
|
||||
...v
|
||||
}); // Tis is an HTML string
|
||||
if (i % 2 === 0) {
|
||||
htmlLeft += appendHtml;
|
||||
} else {
|
||||
htmlRight += appendHtml;
|
||||
}
|
||||
}
|
||||
return `
|
||||
<div class="left questions-left">
|
||||
${htmlLeft}
|
||||
</div>
|
||||
<div class="right questions-right">
|
||||
${htmlRight}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
eq(value1, value2) {
|
||||
return value1 === value2;
|
||||
},
|
||||
neq(value1, value2) {
|
||||
return value1 !== value2;
|
||||
},
|
||||
greaterThan(value1, value2) {
|
||||
return value1 > value2;
|
||||
},
|
||||
slug(string) {
|
||||
return string.toLowerCase().replaceAll(/ /g, '-');
|
||||
},
|
||||
section(name, options) {
|
||||
if (!this._sections) {
|
||||
this._sections = {};
|
||||
}
|
||||
if (!this._sections[name]) {
|
||||
this._sections[name] = [];
|
||||
}
|
||||
this._sections[name].push(options.fn(this));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}));
|
||||
app.set('view engine', 'handlebars');
|
||||
|
||||
logger.info('Starting server');
|
||||
database.connect().then(() => {
|
||||
app.listen(port, async () => {
|
||||
/*
|
||||
const events = await stripe.events.list({
|
||||
delivery_success: false // failed webhooks
|
||||
});
|
||||
|
||||
for (const event of events.data) {
|
||||
await util.handleStripeEvent(event);
|
||||
}
|
||||
*/
|
||||
|
||||
logger.success(`Server listening on http://localhost:${port}`);
|
||||
});
|
||||
});
|
||||
294
src/stripe.js
294
src/stripe.js
@@ -1,294 +0,0 @@
|
||||
const Stripe = require('stripe');
|
||||
const config = require('./config');
|
||||
const mailer = require('./mailer');
|
||||
const util = require('./util');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
|
||||
async function handleStripeEvent(event) {
|
||||
if (event.type === 'customer.subscription.updated' || event.type === 'customer.subscription.deleted') {
|
||||
const subscription = event.data.object;
|
||||
const product = await stripe.products.retrieve(subscription.plan.product);
|
||||
const customer = await stripe.customers.retrieve(subscription.customer);
|
||||
|
||||
if (!customer?.metadata?.pnid_pid) {
|
||||
// No PNID PID linked to customer
|
||||
if (subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
|
||||
// Abort and refund!
|
||||
logger.error(`Stripe user ${customer.id} has no PNID linked! Refunding order`);
|
||||
|
||||
try {
|
||||
await stripe.subscriptions.del(subscription.id);
|
||||
|
||||
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
|
||||
await stripe.refunds.create({
|
||||
payment_intent: invoice.payment_intent
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error refunding subscription | ${customer.id}, ${subscription.id} | - ${error.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription Failed - No Linked PNID',
|
||||
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to no PNID PID being linked to the Stripe customer account used. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.error(`Stripe user ${customer.id} has no PNID linked!`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const pid = Number(customer.metadata.pnid_pid);
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
|
||||
if (!pnid) {
|
||||
// PNID does not exist
|
||||
if (subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
|
||||
// Abort and refund!
|
||||
logger.error(`PNID PID ${pid} does not exist! Found on Stripe user ${customer.id}! Refunding order`);
|
||||
|
||||
try {
|
||||
await stripe.subscriptions.del(subscription.id);
|
||||
|
||||
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
|
||||
await stripe.refunds.create({
|
||||
payment_intent: invoice.payment_intent
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error refunding subscription | ${customer.id}, ${subscription.id} | - ${error.message}`);
|
||||
}
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription Failed - PNID Not Found',
|
||||
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to the provided PNID not being found. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}\nPNID PID: ${pid}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.error(`PNID PID ${pid} does not exist! Found on Stripe user ${customer.id}!`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const latestWebhookTimestamp = pnid.get('connections.stripe.latest_webhook_timestamp');
|
||||
|
||||
if (latestWebhookTimestamp && latestWebhookTimestamp >= event.created) {
|
||||
// Do nothing, this webhook is older than the latest seen
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSubscriptionId = pnid.get('connections.stripe.subscription_id');
|
||||
const discordId = pnid.get('connections.discord.id');
|
||||
|
||||
if (subscription.status === 'canceled' && currentSubscriptionId && subscription.id !== currentSubscriptionId) {
|
||||
// Canceling old subscription, do nothing but update webhook date and remove Discord roles
|
||||
if (product.metadata.beta === 'true') {
|
||||
util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
logger.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
logger.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} | - ${error.message}`);
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.latest_webhook_timestamp': event.created
|
||||
};
|
||||
|
||||
await database.PNID.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: event.created
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.subscription_id': subscription.status === 'active' ? subscription.id : null,
|
||||
'connections.stripe.price_id': subscription.status === 'active' ? subscription.plan.id : null,
|
||||
'connections.stripe.tier_level': subscription.status === 'active' ? Number(product.metadata.tier_level || 0) : 0,
|
||||
'connections.stripe.tier_name': subscription.status === 'active' ? product.name : null,
|
||||
'connections.stripe.latest_webhook_timestamp': event.created
|
||||
};
|
||||
|
||||
if (product.metadata.beta === 'true') {
|
||||
if (subscription.status === 'active') {
|
||||
if (pnid.access_level < 2) { // * Only change access level if not staff member
|
||||
updateData.access_level = 1;
|
||||
updateData.server_access_level = 'test';
|
||||
}
|
||||
|
||||
util.assignDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
logger.error(`Error assigning user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
} else {
|
||||
// * Assume any status other than active means payment has not been fulfilled
|
||||
// * Once the payment goes through, status should update to active
|
||||
if (pnid.access_level < 2) { // * Only change access level if not staff member
|
||||
updateData.access_level = 0;
|
||||
updateData.server_access_level = 'prod';
|
||||
}
|
||||
|
||||
util.removeDiscordMemberTesterRole(discordId).catch((error) => {
|
||||
logger.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await database.PNID.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: event.created
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
|
||||
if (subscription.status === 'active') {
|
||||
// Get all the customers active subscriptions
|
||||
const { data: activeSubscriptions } = await stripe.subscriptions.list({
|
||||
limit: 100,
|
||||
status: 'active',
|
||||
customer: customer.id
|
||||
});
|
||||
|
||||
// Order subscriptions by creation time and remove the latest one
|
||||
const orderedActiveSubscriptions = activeSubscriptions.sort((a, b) => b.created - a.created);
|
||||
const pastSubscriptions = orderedActiveSubscriptions.slice(1);
|
||||
|
||||
// Remove any old past subscriptions that might still be hanging around
|
||||
for (const pastSubscription of pastSubscriptions) {
|
||||
try {
|
||||
await stripe.subscriptions.del(pastSubscription.id);
|
||||
} catch (error) {
|
||||
logger.error(`Error canceling old user subscription | ${customer.id}, ${pid}, ${pastSubscription.id} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: `Pretendo Network ${product.name} Subscription - Active`,
|
||||
text: `Thank you for purchasing the ${product.name} tier! We greatly value your support, thank you for helping keep Pretendo Network alive!\nIt may take a moment for your account dashboard to reflect these changes. Please wait a moment and refresh the dashboard to see them!`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
|
||||
util.assignDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
logger.error(`Error assigning user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} | - ${error.message}`);
|
||||
});
|
||||
|
||||
for (const email of config.stripe.notification_emails) {
|
||||
// * Send notification emails for new sub
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
subject: `[Pretendo] - New ${product.name} subscription`,
|
||||
text: `${pnid.get('username')} just became a ${product.name} tier subscriber`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification email | ${email} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else if (subscription.status === 'canceled') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: `Pretendo Network ${product.name} Subscription - Canceled`,
|
||||
text: `Your subscription for the ${product.name} tier has been canceled. We thank for your previous support, and hope you still enjoy the network! `
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
logger.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} | - ${error.message}`);
|
||||
});
|
||||
|
||||
for (const email of config.stripe.notification_emails) {
|
||||
// * Send notification emails for new sub
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
subject: `[Pretendo] - Canceled ${product.name} subscription`,
|
||||
text: `${pnid.get('username')} just canceled their ${product.name} tier subscription`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification email | ${email} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else if (subscription.status === 'unpaid') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: `Pretendo Network ${product.name} Subscription - Unpaid`,
|
||||
text: `Your subscription for the ${product.name} tier has been canceled due to non payment. We thank for your previous support, and hope you still enjoy the network! `
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
logger.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} | - ${error.message}`);
|
||||
});
|
||||
|
||||
for (const email of config.stripe.notification_emails) {
|
||||
// * Send notification emails for new sub
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
subject: `[Pretendo] - Removed ${product.name} subscription`,
|
||||
text: `${pnid.get('username')}'s ${product.name} tier subscription has been canceled due to non payment`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification email | ${email} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: `Pretendo Network ${product.name} Subscription - ${subscription.status}`,
|
||||
text: `Your subscription for the ${product.name} tier has changed status to ${subscription.status}. This is usually caused by payment failure. Your account has been reverted back to default until payment resumes. If you believe this to be an error, please reach out for support on our Discord server, and we thank you for your previous support!`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
|
||||
util.removeDiscordMemberSupporterRole(discordId, product.metadata.discord_role_id).catch((error) => {
|
||||
logger.error(`Error removing user Discord supporter role | ${customer.id}, ${discordId}, ${pid}, ${product.metadata.discord_role_id} | - ${error.message}`);
|
||||
});
|
||||
|
||||
for (const email of config.stripe.notification_emails) {
|
||||
// * Send notification emails for new sub
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: email,
|
||||
subject: `[Pretendo] - Removed ${product.name} subscription`,
|
||||
text: `${pnid.username}'s ${product.name} tier subscription status has been changed to ${subscription.status}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending notification email | ${email} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleStripeEvent
|
||||
};
|
||||
284
src/util.js
284
src/util.js
@@ -1,284 +0,0 @@
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const { marked } = require('marked');
|
||||
const { REST: DiscordRest } = require('@discordjs/rest');
|
||||
const { Routes: DiscordRoutes } = require('discord-api-types/v10');
|
||||
const merge = require('lodash.merge');
|
||||
const config = require('./config');
|
||||
const logger = require('./logger');
|
||||
const baseLocale = require(`${__dirname}/../locales/en_US.json`);
|
||||
|
||||
const discordRest = new DiscordRest({ version: '10' }).setToken(config.discord.bot_token);
|
||||
|
||||
function fullUrl(request) {
|
||||
return `${request.protocol}://${request.hostname}${request.originalUrl}`;
|
||||
}
|
||||
|
||||
function getLocale(locale) {
|
||||
const localeFileName = locale.replace('-', '_');
|
||||
|
||||
const path = `${__dirname}/../locales/${localeFileName}.json`;
|
||||
|
||||
if (fs.pathExistsSync(path)) {
|
||||
const selectedLocale = require(path);
|
||||
const finalLocale = merge({}, baseLocale, selectedLocale);
|
||||
|
||||
return finalLocale;
|
||||
}
|
||||
|
||||
logger.warn(`Could not find locale ${localeFileName}! Loading en_US`);
|
||||
|
||||
return baseLocale;
|
||||
}
|
||||
|
||||
function getRawDocs(locale, subpath, pageName) {
|
||||
const localePath = path.join(__dirname, '../docs', locale.replace('-', '_'), subpath, `${pageName}.md`);
|
||||
const defaultPath = path.join(__dirname, '../docs', 'en_US', subpath, `${pageName}.md`);
|
||||
|
||||
if (fs.existsSync(localePath)) {
|
||||
return {
|
||||
content: parseDocs(fs.readFileSync(localePath, 'utf8')),
|
||||
MDLocale: locale
|
||||
};
|
||||
} else if (fs.existsSync(defaultPath)) {
|
||||
return {
|
||||
content: parseDocs(fs.readFileSync(defaultPath, 'utf8')),
|
||||
MDLocale: 'en-US'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: null,
|
||||
MDLocale: null
|
||||
};
|
||||
}
|
||||
|
||||
function parseDocs(rawDocs) {
|
||||
if (!rawDocs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let markedContent = marked(rawDocs);
|
||||
markedContent = markedContent.replaceAll(/\[yt-iframe\]\(.{11}\)/g, (match) => {
|
||||
const videoIDRegex = /(?<=\[yt-iframe\]\().*(?=\))/g;
|
||||
const videoID = match.match(videoIDRegex)[0];
|
||||
return `<div class="aspectratio-fallback"><iframe src="https://www.youtube-nocookie.com/embed/${videoID}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>`;
|
||||
});
|
||||
|
||||
const htmlContent = marked.parse(markedContent);
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
function apiGetRequest(path, headers) {
|
||||
return got.get(`${config.api_base}${path}`, {
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers
|
||||
});
|
||||
}
|
||||
|
||||
function apiPostRequest(path, headers, json) {
|
||||
return got.post(`${config.api_base}${path}`, {
|
||||
responseType: 'json',
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers,
|
||||
json
|
||||
});
|
||||
}
|
||||
|
||||
function apiDeleteRequest(path, headers, json) {
|
||||
return got.delete(`${config.api_base}${path}`, {
|
||||
throwHttpErrors: false,
|
||||
https: {
|
||||
rejectUnauthorized: false // Needed for self-signed certificates on localhost testing
|
||||
},
|
||||
headers,
|
||||
json
|
||||
});
|
||||
}
|
||||
|
||||
async function register(registerData) {
|
||||
const apiResponse = await apiPostRequest('/v1/register', {}, registerData);
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
throw new Error(apiResponse.body.error);
|
||||
}
|
||||
|
||||
return apiResponse.body;
|
||||
}
|
||||
|
||||
async function login(username, password) {
|
||||
const apiResponse = await apiPostRequest('/v1/login', {}, {
|
||||
username,
|
||||
password,
|
||||
grant_type: 'password'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
throw new Error(apiResponse.body.error);
|
||||
}
|
||||
|
||||
return apiResponse.body;
|
||||
}
|
||||
|
||||
async function forgotPassword(forgotPasswordData) {
|
||||
const apiResponse = await apiPostRequest('/v1/forgot-password', {}, forgotPasswordData);
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
throw new Error(apiResponse.body.error);
|
||||
}
|
||||
|
||||
return apiResponse.body;
|
||||
}
|
||||
|
||||
async function refreshLogin(request, response) {
|
||||
const apiResponse = await apiPostRequest('/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' });
|
||||
|
||||
request.cookies.refresh_token = tokens.refresh_token;
|
||||
request.cookies.access_token = tokens.access_token;
|
||||
request.cookies.token_type = tokens.token_type;
|
||||
}
|
||||
|
||||
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 apiPostRequest('/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);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDiscordConnection(request, response, fromRetry = false) {
|
||||
const apiResponse = await apiDeleteRequest('/v1/connections/remove/discord', {
|
||||
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);
|
||||
await removeDiscordConnection(request, response, true);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function assignDiscordMemberSupporterRole(memberId, roleId) {
|
||||
if (memberId && memberId.trim() !== '') {
|
||||
await discordRest.put(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, config.discord.roles.supporter));
|
||||
await discordRest.put(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, roleId));
|
||||
}
|
||||
}
|
||||
|
||||
async function assignDiscordMemberTesterRole(memberId) {
|
||||
if (memberId && memberId.trim() !== '') {
|
||||
await discordRest.put(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, config.discord.roles.tester));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDiscordMemberSupporterRole(memberId, roleId) {
|
||||
if (memberId && memberId.trim() !== '') {
|
||||
await discordRest.delete(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, config.discord.roles.supporter));
|
||||
await discordRest.delete(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, roleId));
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDiscordMemberTesterRole(memberId) {
|
||||
if (memberId && memberId.trim() !== '') {
|
||||
await discordRest.delete(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, config.discord.roles.tester));
|
||||
}
|
||||
}
|
||||
|
||||
function signDiscoursePayload(payload) {
|
||||
return crypto.createHmac('sha256', config.discourse.sso.secret).update(payload).digest('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fullUrl,
|
||||
getLocale,
|
||||
getRawDocs,
|
||||
parseDocs,
|
||||
apiGetRequest,
|
||||
apiPostRequest,
|
||||
apiDeleteRequest,
|
||||
register,
|
||||
login,
|
||||
forgotPassword,
|
||||
refreshLogin,
|
||||
getUserAccountData,
|
||||
updateDiscordConnection,
|
||||
removeDiscordConnection,
|
||||
nintendoPasswordHash,
|
||||
assignDiscordMemberSupporterRole,
|
||||
assignDiscordMemberTesterRole,
|
||||
removeDiscordMemberSupporterRole,
|
||||
removeDiscordMemberTesterRole,
|
||||
signDiscoursePayload
|
||||
};
|
||||
Reference in New Issue
Block a user