Added password reset support

This commit is contained in:
Jonathan Barrow 2022-10-01 17:00:57 -04:00
parent 4ed31a6871
commit be3d62a836
No known key found for this signature in database
GPG Key ID: E86E9FE9049C741F
12 changed files with 403 additions and 5 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)",

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,26 @@
const config = require('../../../config.json');
const input = document.querySelector('#input');
document.querySelector('form').addEventListener('submit', function (event) {
event.preventDefault();
fetch(`${config.api_base}/v1/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

@ -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,18 @@ router.get('/logout', async(_request, response) => {
response.redirect('/');
});
router.get('/forgot-password', async (request, response) => {
response.render('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;

View File

@ -151,7 +151,7 @@ database.connect().then(() => {
});
for (const event of events.data) {
await util.handleStripeEvent(event);
//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

@ -15,7 +15,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>