mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-27 11:05:31 -05:00
@@ -261,6 +261,19 @@
|
||||
"registerPrompt": "Don't have an account?",
|
||||
"loginPrompt": "Already have an account?"
|
||||
},
|
||||
"forgotPassword": {
|
||||
"header": "Forgot Password",
|
||||
"sub": "Enter your email address/PNID below",
|
||||
"input": "Email address or PNID",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"resetPassword": {
|
||||
"header": "Reset Password",
|
||||
"sub": "Enter new password below",
|
||||
"password": "Password",
|
||||
"confirmPassword": "Confirm password",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"settings": {
|
||||
"downloadFiles": "Download account files",
|
||||
"downloadFilesDescription": "(will not work on Nintendo Network)",
|
||||
|
||||
898
package-lock.json
generated
898
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,11 @@
|
||||
"description": "",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "browserify ./public/assets/js/miieditor.js -o ./public/assets/js/miieditor.bundled.js && node src/server.js"
|
||||
"start": "npm run build && node src/server.js",
|
||||
"build": "npm run build-miieditor && npm run build-forgot-password && npm run build-reset-password",
|
||||
"build-miieditor": "browserify ./public/assets/js/miieditor.js -o ./public/assets/js/miieditor.bundled.js",
|
||||
"build-forgot-password": "browserify ./public/assets/js/forgot-password.js -o ./public/assets/js/forgot-password.bundled.js",
|
||||
"build-reset-password": "browserify ./public/assets/js/reset-password.js -o ./public/assets/js/reset-password.bundled.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -31,7 +35,7 @@
|
||||
"got": "^11.8.5",
|
||||
"graphql-request": "^4.3.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"marked": "^4.0.10",
|
||||
"mii-js": "github:PretendoNetwork/mii-js#v1.0.4",
|
||||
"mongoose": "^6.4.0",
|
||||
|
||||
132
public/assets/css/forgot-password.css
Normal file
132
public/assets/css/forgot-password.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
margin: 35px 0;
|
||||
}
|
||||
|
||||
.account-form-wrapper {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.account {
|
||||
display: block;
|
||||
padding: 40px 48px;
|
||||
background-color: var(--bg-shade-2);
|
||||
color: var(--text-shade-1);
|
||||
border-radius: 12px;
|
||||
width: min(480px, 90vw);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form.account h2 {
|
||||
margin: 0;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
form.account div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
form.account label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
form.account button {
|
||||
width: 100%;
|
||||
background: var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.account a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: var(--text-shade-1);
|
||||
text-align: right;
|
||||
margin: 6px 0;
|
||||
width: fit-content;
|
||||
}
|
||||
form.account a:hover {
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account a.pwdreset {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
form.account a.register {
|
||||
margin: auto;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@keyframes banner-notice {
|
||||
0% {
|
||||
top: -150px;
|
||||
}
|
||||
20% {
|
||||
top: 35px;
|
||||
}
|
||||
80% {
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
top: -150px;
|
||||
}
|
||||
}
|
||||
.banner-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: -150px;
|
||||
width: 100%;
|
||||
animation: banner-notice 5s;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.success div {
|
||||
background: var(--green-shade-0);
|
||||
}
|
||||
|
||||
form.account.register {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: min(780px, 90vw);
|
||||
column-gap: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
form.account.register div.h-captcha {
|
||||
grid-column: 1 / span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
form.account.register {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
form.account.register div.h-captcha,
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: unset;
|
||||
}
|
||||
}
|
||||
132
public/assets/css/reset-password.css
Normal file
132
public/assets/css/reset-password.css
Normal file
@@ -0,0 +1,132 @@
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
header {
|
||||
margin: 35px 0;
|
||||
}
|
||||
|
||||
.account-form-wrapper {
|
||||
margin: auto;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
form.account {
|
||||
display: block;
|
||||
padding: 40px 48px;
|
||||
background-color: var(--bg-shade-2);
|
||||
color: var(--text-shade-1);
|
||||
border-radius: 12px;
|
||||
width: min(480px, 90vw);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form.account h2 {
|
||||
margin: 0;
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account p {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
form.account div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
form.account label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
form.account button {
|
||||
width: 100%;
|
||||
background: var(--accent-shade-0);
|
||||
}
|
||||
|
||||
form.account a {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
color: var(--text-shade-1);
|
||||
text-align: right;
|
||||
margin: 6px 0;
|
||||
width: fit-content;
|
||||
}
|
||||
form.account a:hover {
|
||||
color: var(--text-shade-3);
|
||||
}
|
||||
|
||||
form.account a.pwdreset {
|
||||
margin-left: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
form.account a.register {
|
||||
margin: auto;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
@keyframes banner-notice {
|
||||
0% {
|
||||
top: -150px;
|
||||
}
|
||||
20% {
|
||||
top: 35px;
|
||||
}
|
||||
80% {
|
||||
top: 35px;
|
||||
}
|
||||
100% {
|
||||
top: -150px;
|
||||
}
|
||||
}
|
||||
.banner-notice {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: -150px;
|
||||
width: 100%;
|
||||
animation: banner-notice 5s;
|
||||
}
|
||||
.banner-notice div {
|
||||
padding: 4px 36px;
|
||||
border-radius: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
.banner-notice.success div {
|
||||
background: var(--green-shade-0);
|
||||
}
|
||||
|
||||
form.account.register {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
width: min(780px, 90vw);
|
||||
column-gap: 24px;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
form.account.register div.h-captcha {
|
||||
grid-column: 1 / span 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px) {
|
||||
form.account.register {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
form.account.register div.h-captcha,
|
||||
form.account.register p,
|
||||
form.account.register div.email,
|
||||
form.account.register div.buttons {
|
||||
grid-column: unset;
|
||||
}
|
||||
}
|
||||
24
public/assets/js/forgot-password.js
Normal file
24
public/assets/js/forgot-password.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const input = document.querySelector('#input');
|
||||
document.querySelector('form').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
fetch('/account/forgot-password', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: input.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(body => {
|
||||
if (body.error) {
|
||||
alert(`Error: ${body.error}. TODO: red error message thing`);
|
||||
} else {
|
||||
alert('If an account exists with the provided username/email address an email has been sent. TODO: reword this and green success');
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
});
|
||||
32
public/assets/js/reset-password.js
Normal file
32
public/assets/js/reset-password.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const config = require('../../../config.json');
|
||||
|
||||
const passwordInput = document.querySelector('#password');
|
||||
const passwordConfirmInput = document.querySelector('#password_confirm');
|
||||
const tokenInput = document.querySelector('#token');
|
||||
|
||||
document.querySelector('form').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
|
||||
fetch(`${config.api_base}/v1/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: passwordInput.value,
|
||||
password_confirm: passwordConfirmInput.value,
|
||||
token: tokenInput.value
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(body => {
|
||||
if (body.error) {
|
||||
alert(`Error: ${body.error}. TODO: red error message thing`);
|
||||
} else {
|
||||
alert('Password reset. TODO: reword this and green success');
|
||||
window.location.assign('/account/login');
|
||||
}
|
||||
})
|
||||
.catch(console.log);
|
||||
});
|
||||
@@ -3,12 +3,12 @@ const PNIDSchema = require('./schema/pnid');
|
||||
const config = require('../config.json');
|
||||
|
||||
const accountServerConfig = config.database.account;
|
||||
const { uri, database, options } = accountServerConfig;
|
||||
const { connection_string, options } = accountServerConfig;
|
||||
let accountServerDBConnection;
|
||||
let PNID;
|
||||
|
||||
async function connect() {
|
||||
accountServerDBConnection = await mongoose.createConnection(`${uri}/${database}`, options);
|
||||
accountServerDBConnection = await mongoose.createConnection(connection_string, options);
|
||||
accountServerDBConnection.on('error', console.error.bind(console, 'Mongoose connection error:'));
|
||||
accountServerDBConnection.on('close', () => {
|
||||
accountServerDBConnection.removeAllListeners();
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 config = require('../../config.json');
|
||||
const editorJSON = require('../json/miieditor.json');
|
||||
@@ -143,7 +144,7 @@ router.post('/register', async (request, response) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/logout', async(_request, response) => {
|
||||
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' });
|
||||
@@ -151,6 +152,23 @@ router.get('/logout', async(_request, response) => {
|
||||
response.redirect('/');
|
||||
});
|
||||
|
||||
router.get('/forgot-password', async (request, response) => {
|
||||
response.render('account/forgot-password');
|
||||
});
|
||||
|
||||
router.post('/forgot-password', async (request, response) => {
|
||||
const apiResponse = await util.apiPostRequest('/v1/forgot-password', {}, request.body);
|
||||
response.json(apiResponse.body);
|
||||
});
|
||||
|
||||
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;
|
||||
@@ -215,7 +233,7 @@ router.get('/remove/discord', requireLoginMiddleware, async (request, response)
|
||||
await util.removeDiscordMemberTesterRole(discordId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response.cookie('success_message', 'Discord account removed successfully', { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
} catch (error) {
|
||||
@@ -426,7 +444,7 @@ router.post('/stripe/webhook', async (request, response) => {
|
||||
return response.status(400).send(`Webhook Error: ${error.message}`);
|
||||
}
|
||||
|
||||
await util.handleStripeEvent(event);
|
||||
await handleStripeEvent(event);
|
||||
|
||||
response.json({ received: true });
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ const handlebars = require('express-handlebars');
|
||||
const morgan = require('morgan');
|
||||
const expressLocale = require('express-locale');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const Stripe = require('stripe');
|
||||
//const Stripe = require('stripe');
|
||||
const redirectMiddleware = require('./middleware/redirect');
|
||||
const renderDataMiddleware = require('./middleware/render-data');
|
||||
const database = require('./database');
|
||||
@@ -15,7 +15,7 @@ const config = require('../config.json');
|
||||
|
||||
const { http: { port } } = config;
|
||||
const app = express();
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
//const stripe = new Stripe(config.stripe.secret_key);
|
||||
|
||||
logger.info('Setting up Middleware');
|
||||
app.use(morgan('dev'));
|
||||
@@ -152,6 +152,7 @@ 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
|
||||
});
|
||||
@@ -159,6 +160,7 @@ database.connect().then(() => {
|
||||
for (const event of events.data) {
|
||||
await util.handleStripeEvent(event);
|
||||
}
|
||||
*/
|
||||
|
||||
logger.success(`Server listening on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
277
src/stripe.js
Normal file
277
src/stripe.js
Normal file
@@ -0,0 +1,277 @@
|
||||
const Stripe = require('stripe');
|
||||
const mailer = require('./mailer');
|
||||
const util = require('./util');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
const config = require('../config.json');
|
||||
|
||||
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') {
|
||||
switch (subscription.status) {
|
||||
case '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}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'canceled': // Subscription was canceled
|
||||
case 'unpaid': // User missed too many payments
|
||||
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}`);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'canceled') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'unpaid') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleStripeEvent
|
||||
};
|
||||
273
src/util.js
273
src/util.js
@@ -2,21 +2,15 @@ const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const crypto = require('crypto');
|
||||
const Stripe = require('stripe');
|
||||
const { marked } = require('marked');
|
||||
const { REST: DiscordRest } = require('@discordjs/rest');
|
||||
const { Routes: DiscordRoutes } = require('discord-api-types/v10');
|
||||
const merge = require('lodash.merge');
|
||||
|
||||
const mailer = require('./mailer');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
|
||||
const config = require('../config.json');
|
||||
const baseLocale = require(`${__dirname}/../locales/en_US.json`);
|
||||
const discordRest = new DiscordRest({ version: '10' }).setToken(config.discord.bot_token);
|
||||
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
const discordRest = new DiscordRest({ version: '10' }).setToken(config.discord.bot_token);
|
||||
|
||||
function fullUrl(request) {
|
||||
return `${request.protocol}://${request.hostname}${request.originalUrl}`;
|
||||
@@ -225,270 +219,6 @@ function nintendoPasswordHash(password, pid) {
|
||||
return hashed;
|
||||
}
|
||||
|
||||
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') {
|
||||
removeDiscordMemberTesterRole(discordId).catch(error => {
|
||||
logger.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
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') {
|
||||
switch (subscription.status) {
|
||||
case 'active':
|
||||
if (pnid.access_level < 2) { // only change access level if not staff member
|
||||
updateData.access_level = 1;
|
||||
}
|
||||
|
||||
assignDiscordMemberTesterRole(discordId).catch(error => {
|
||||
logger.error(`Error assigning user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'canceled': // Subscription was canceled
|
||||
case 'unpaid': // User missed too many payments
|
||||
if (pnid.access_level < 2) { // only change access level if not staff member
|
||||
updateData.access_level = 0;
|
||||
}
|
||||
|
||||
removeDiscordMemberTesterRole(discordId).catch(error => {
|
||||
logger.error(`Error removing user Discord tester role | ${customer.id}, ${discordId}, ${pid} | - ${error.message}`);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 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}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'canceled') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network 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}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'unpaid') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network 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}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function assignDiscordMemberSupporterRole(memberId, roleId) {
|
||||
if (memberId && memberId.trim() !== '') {
|
||||
await discordRest.put(DiscordRoutes.guildMemberRole(config.discord.guild_id, memberId, config.discord.roles.supporter));
|
||||
@@ -530,7 +260,6 @@ module.exports = {
|
||||
updateDiscordConnection,
|
||||
removeDiscordConnection,
|
||||
nintendoPasswordHash,
|
||||
handleStripeEvent,
|
||||
assignDiscordMemberSupporterRole,
|
||||
assignDiscordMemberTesterRole,
|
||||
removeDiscordMemberSupporterRole,
|
||||
|
||||
22
views/account/forgot-password.handlebars
Normal file
22
views/account/forgot-password.handlebars
Normal file
@@ -0,0 +1,22 @@
|
||||
<link rel="stylesheet" href="/assets/css/forgot-password.css" />
|
||||
|
||||
{{> header}}
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<div class="account-form-wrapper">
|
||||
<form method="post" class="account">
|
||||
<h2>{{ locale.account.forgotPassword.header }}</h2>
|
||||
<p>{{ locale.account.forgotPassword.sub }}</p>
|
||||
<div>
|
||||
<label for="input">{{ locale.account.forgotPassword.input }}</label>
|
||||
<input name="input" id="input" required>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button type="submit">{{ locale.account.forgotPassword.submit }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/forgot-password.bundled.js"></script>
|
||||
@@ -17,7 +17,7 @@
|
||||
<div>
|
||||
<label for="password">{{ locale.account.loginForm.password }}</label>
|
||||
<input name="password" id="password" type="password" required>
|
||||
<a href="/account/passwordreset" class="pwdreset">{{ locale.account.loginForm.forgotPassword }}</a>
|
||||
<a href="/account/forgot-password" class="pwdreset">{{ locale.account.loginForm.forgotPassword }}</a>
|
||||
</div>
|
||||
<input name="grant_type" id="grant_type" type="hidden" value="password">
|
||||
<input name="redirect" id="redirect" type="hidden" value="{{redirect}}">
|
||||
|
||||
26
views/account/reset-password.handlebars
Normal file
26
views/account/reset-password.handlebars
Normal file
@@ -0,0 +1,26 @@
|
||||
<link rel="stylesheet" href="/assets/css/reset-password.css" />
|
||||
|
||||
{{> header}}
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="account-form-wrapper">
|
||||
<form method="post" class="account">
|
||||
<h2>{{ locale.account.resetPassword.header }}</h2>
|
||||
<p>{{ locale.account.resetPassword.sub }}</p>
|
||||
<div>
|
||||
<label for="password">{{ locale.account.resetPassword.password }}</label>
|
||||
<input name="password" id="password" type="password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password_confirm">{{ locale.account.resetPassword.confirmPassword }}</label>
|
||||
<input name="password_confirm" id="password_confirm" type="password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<input name="token" id="token" type="hidden" value="{{ token }}">
|
||||
<div class="buttons">
|
||||
<button type="submit">{{ locale.account.resetPassword.submit }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/assets/js/reset-password.bundled.js"></script>
|
||||
Reference in New Issue
Block a user