Merge pull request #177 from PretendoNetwork/feature-forgot-password

[Feature] Password resetting
This commit is contained in:
Jonathan Barrow 2023-02-25 23:00:42 -05:00 committed by GitHub
commit 53c6711c02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 418 additions and 15 deletions

View File

@ -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)",

12
package-lock.json generated
View File

@ -3186,9 +3186,9 @@
}
},
"node_modules/neo-async": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"node_modules/node-fetch": {
"version": "2.6.7",
@ -7022,9 +7022,9 @@
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
"neo-async": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz",
"integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw=="
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
},
"node-fetch": {
"version": "2.6.7",

View File

@ -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",

View 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;
}
}

View 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;
}
}

View 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);
});

View 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);
});

View File

@ -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();

View File

@ -143,7 +143,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 +151,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 +232,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) {

View File

@ -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}`);
});

View File

@ -284,7 +284,6 @@ async function handleStripeEvent(event) {
logger.error(`Error refunding subscription | ${customer.id}, ${subscription.id} | - ${error.message}`);
}
try {
await mailer.sendMail({
to: customer.email,

View 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>

View File

@ -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}}">

View 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>