mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-08-28 05:44:15 -05:00
Added password reset endpoints for website
This commit is contained in:
@@ -28,5 +28,6 @@
|
||||
"hcaptcha": {
|
||||
"secret": "0x0000000000000000000000000000000000000000"
|
||||
},
|
||||
"cdn_base": "https://example.com"
|
||||
"cdn_base": "https://example.com",
|
||||
"website_base": "https://example.com"
|
||||
}
|
||||
16
package-lock.json
generated
16
package-lock.json
generated
@@ -36,6 +36,7 @@
|
||||
"nodemailer": "^6.4.2",
|
||||
"redis": "^4.3.1",
|
||||
"tga": "^1.0.4",
|
||||
"validator": "^13.7.0",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
},
|
||||
@@ -3561,6 +3562,14 @@
|
||||
"uuid": "bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
@@ -5378,7 +5387,7 @@
|
||||
},
|
||||
"mii-js": {
|
||||
"version": "git+ssh://git@github.com/PretendoNetwork/mii-js.git#5d8eb8013514a13b0df6eb4a5bfd8b5a63fb9861",
|
||||
"from": "mii-js@https://github.com/PretendoNetwork/mii-js",
|
||||
"from": "mii-js@github:PretendoNetwork/mii-js",
|
||||
"requires": {
|
||||
"bit-buffer": "^0.2.5"
|
||||
}
|
||||
@@ -6514,6 +6523,11 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
|
||||
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
|
||||
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
|
||||
},
|
||||
"vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"nodemailer": "^6.4.2",
|
||||
"redis": "^4.3.1",
|
||||
"tga": "^1.0.4",
|
||||
"validator": "^13.7.0",
|
||||
"xmlbuilder": "^13.0.2",
|
||||
"xmlbuilder2": "0.0.4"
|
||||
},
|
||||
|
||||
@@ -54,6 +54,16 @@ async function getUserByPID(pid) {
|
||||
return user;
|
||||
}
|
||||
|
||||
async function getUserByEmailAddress(email) {
|
||||
verifyConnected();
|
||||
|
||||
const user = await PNID.findOne({
|
||||
'email.address': email.toLowerCase()
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async function doesUserExist(username) {
|
||||
verifyConnected();
|
||||
|
||||
@@ -252,6 +262,7 @@ module.exports = {
|
||||
connection,
|
||||
getUserByUsername,
|
||||
getUserByPID,
|
||||
getUserByEmailAddress,
|
||||
doesUserExist,
|
||||
getUserBasic,
|
||||
getUserBearer,
|
||||
|
||||
@@ -19,8 +19,10 @@ api.options('*', cors());
|
||||
logger.info('[USER API] Applying imported routes');
|
||||
api.use('/v1/connections', routes.V1.CONNECTIONS);
|
||||
api.use('/v1/email', routes.V1.EMAIL);
|
||||
api.use('/v1/forgot-password', routes.V1.FORGOT_PASSWORD);
|
||||
api.use('/v1/login', routes.V1.LOGIN);
|
||||
api.use('/v1/register', routes.V1.REGISTER);
|
||||
api.use('/v1/reset-password', routes.V1.RESET_PASSWORD);
|
||||
api.use('/v1/user', routes.V1.USER);
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@ module.exports = {
|
||||
V1: {
|
||||
CONNECTIONS: require('./v1/connections'),
|
||||
EMAIL: require('./v1/email'),
|
||||
FORGOT_PASSWORD: require('./v1/forgotPassword'),
|
||||
LOGIN: require('./v1/login'),
|
||||
REGISTER: require('./v1/register'),
|
||||
RESET_PASSWORD: require('./v1/resetPassword'),
|
||||
USER: require('./v1/user'),
|
||||
}
|
||||
};
|
||||
36
src/services/api/routes/v1/forgotPassword.js
Normal file
36
src/services/api/routes/v1/forgotPassword.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const router = require('express').Router();
|
||||
const validator = require('validator');
|
||||
const database = require('../../../../database');
|
||||
const util = require('../../../../util');
|
||||
|
||||
router.post('/', async (request, response) => {
|
||||
const { body } = request;
|
||||
const { input } = body;
|
||||
|
||||
if (!input || input.trim() === '') {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid or missing input'
|
||||
});
|
||||
}
|
||||
|
||||
let pnid;
|
||||
|
||||
if (validator.isEmail(input)) {
|
||||
pnid = await database.getUserByEmailAddress(input);
|
||||
} else {
|
||||
pnid = await database.getUserByUsername(input);
|
||||
}
|
||||
|
||||
if (pnid) {
|
||||
await util.sendForgotPasswordEmail(pnid);
|
||||
}
|
||||
|
||||
response.json({
|
||||
app: 'api',
|
||||
status: 200
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -261,7 +261,7 @@ router.post('/', async (request, response) => {
|
||||
country: 'US', // TODO: Change this
|
||||
language: 'en', // TODO: Change this
|
||||
email: {
|
||||
address: email,
|
||||
address: email.toLowerCase(),
|
||||
primary: true, // TODO: Change this
|
||||
parent: true, // TODO: Change this
|
||||
reachable: false, // TODO: Change this
|
||||
|
||||
127
src/services/api/routes/v1/resetPassword.js
Normal file
127
src/services/api/routes/v1/resetPassword.js
Normal file
@@ -0,0 +1,127 @@
|
||||
const router = require('express').Router();
|
||||
const bcrypt = require('bcrypt');
|
||||
const { PNID } = require('../../../../models/pnid');
|
||||
const util = require('../../../../util');
|
||||
|
||||
// This sucks
|
||||
const PASSWORD_WORD_OR_NUMBER_REGEX = /(?=.*[a-zA-Z])(?=.*\d).*/;
|
||||
const PASSWORD_WORD_OR_PUNCTUATION_REGEX = /(?=.*[a-zA-Z])(?=.*[\_\-\.]).*/;
|
||||
const PASSWORD_NUMBER_OR_PUNCTUATION_REGEX = /(?=.*\d)(?=.*[\_\-\.]).*/;
|
||||
const PASSWORD_REPEATED_CHARACTER_REGEX = /(.)\1\1/;
|
||||
|
||||
router.post('/', async (request, response) => {
|
||||
const { body } = request;
|
||||
const password = body.password?.trim();
|
||||
const passwordConfirm = body.password_confirm?.trim();
|
||||
const token = body.token?.trim();
|
||||
|
||||
if (!token || token === '') {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Missing token'
|
||||
});
|
||||
}
|
||||
|
||||
let unpackedToken;
|
||||
try {
|
||||
const decryptedToken = await util.decryptToken(Buffer.from(token, 'base64'));
|
||||
unpackedToken = util.unpackToken(decryptedToken);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
}
|
||||
|
||||
if (unpackedToken.expire_time < Date.now()) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Token expired'
|
||||
});
|
||||
}
|
||||
|
||||
const pnid = await PNID.findOne({ pid: unpackedToken.pid });
|
||||
|
||||
if (!pnid) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Invalid token. No user found'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (!password || password === '') {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Must enter a password'
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Password is too short'
|
||||
});
|
||||
}
|
||||
|
||||
if (password.length > 16) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Password is too long'
|
||||
});
|
||||
}
|
||||
|
||||
if (password.toLowerCase() === pnid.get('usernameLower')) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Password cannot be the same as username'
|
||||
});
|
||||
}
|
||||
|
||||
if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(password) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(password) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(password)) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Password must have combination of letters, numbers, and/or punctuation characters'
|
||||
});
|
||||
}
|
||||
|
||||
if (PASSWORD_REPEATED_CHARACTER_REGEX.test(password)) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Password may not have 3 repeating characters'
|
||||
});
|
||||
}
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
return response.status(400).json({
|
||||
app: 'api',
|
||||
status: 400,
|
||||
error: 'Passwords do not match'
|
||||
});
|
||||
}
|
||||
|
||||
const primaryPasswordHash = util.nintendoPasswordHash(password, pnid.get('pid'));
|
||||
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.save();
|
||||
|
||||
response.json({
|
||||
app: 'api',
|
||||
status: 200
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -114,7 +114,7 @@ router.post('/', ratelimit, deviceCertificateMiddleware, async (request, respons
|
||||
country: person.get('country'),
|
||||
language: person.get('language'),
|
||||
email: {
|
||||
address: person.get('email').get('address'),
|
||||
address: person.get('email').get('address').toLowerCase(),
|
||||
primary: person.get('email').get('primary') === 'Y',
|
||||
parent: person.get('email').get('parent') === 'Y',
|
||||
reachable: false,
|
||||
@@ -474,7 +474,7 @@ router.put('/@me/emails/@primary', async (request, response) => {
|
||||
}).end());
|
||||
}
|
||||
|
||||
pnid.set('email.address', email.get('address'));
|
||||
pnid.set('email.address', email.get('address').toLowerCase());
|
||||
pnid.set('email.reachable', false);
|
||||
pnid.set('email.validated', false);
|
||||
pnid.set('email.validated_date', '');
|
||||
|
||||
30
src/util.js
30
src/util.js
@@ -245,6 +245,33 @@ async function sendEmailConfirmedEmail(pnid) {
|
||||
});
|
||||
}
|
||||
|
||||
async function sendForgotPasswordEmail(pnid) {
|
||||
const publicKey = await cache.getServicePublicKey('account');
|
||||
const secretKey = await cache.getServiceSecretKey('account');
|
||||
|
||||
const cryptoOptions = {
|
||||
public_key: publicKey,
|
||||
hmac_secret: secretKey
|
||||
};
|
||||
|
||||
const tokenOptions = {
|
||||
system_type: 0xF, // API
|
||||
token_type: 0x5, // Password reset
|
||||
pid: pnid.get('pid'),
|
||||
access_level: pnid.get('access_level'),
|
||||
title_id: BigInt(0),
|
||||
expire_time: BigInt(Date.now() + (24 * 60 * 60 * 1000)) // Only valid for 24 hours
|
||||
};
|
||||
|
||||
const passwordResetToken = await generateToken(cryptoOptions, tokenOptions);
|
||||
|
||||
await mailer.sendMail({
|
||||
to: pnid.get('email.address'),
|
||||
subject: '[Pretendo Network] Forgot Password',
|
||||
html: `Visit this link to reset your password ${config.website_base}/account/reset-password?token=${encodeURIComponent(passwordResetToken)}`
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
nintendoPasswordHash,
|
||||
nintendoBase64Decode,
|
||||
@@ -256,5 +283,6 @@ module.exports = {
|
||||
uploadCDNAsset,
|
||||
nascError,
|
||||
sendConfirmationEmail,
|
||||
sendEmailConfirmedEmail
|
||||
sendEmailConfirmedEmail,
|
||||
sendForgotPasswordEmail
|
||||
};
|
||||
Reference in New Issue
Block a user