mirror of
https://github.com/PretendoNetwork/account.git
synced 2026-10-01 14:48:25 -05:00
Merge pull request #357 from PretendoNetwork/feat/update-password
Feat: update password endpoint, send email notice of password change
This commit is contained in:
8
package-lock.json
generated
8
package-lock.json
generated
@@ -12,7 +12,7 @@
|
||||
"@aws-sdk/client-s3": "^3.657.0",
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@inquirer/prompts": "^7.2.0",
|
||||
"@pretendonetwork/grpc": "^2.5.10",
|
||||
"@pretendonetwork/grpc": "^2.6.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"colors": "^1.4.0",
|
||||
@@ -1680,9 +1680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pretendonetwork/grpc": {
|
||||
"version": "2.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.10.tgz",
|
||||
"integrity": "sha512-1nd/nsRU+tdi08O6fHFuYvUJqByJh/qToQxvsDgWonwZLR/RsBFi3o2MU1/4ketbeGu2KmStLN7vBLDcQ+c44w==",
|
||||
"version": "2.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.6.1.tgz",
|
||||
"integrity": "sha512-+HKJ8cTV4AV8PJgUqg2Tocz4EJTajqErY9r/Oul8WIZM2lO+0gRPSf3Mlxi9Ghvjq88ZS9pWWccAKtn77ePrew==",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.2",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@aws-sdk/client-s3": "^3.657.0",
|
||||
"@aws-sdk/client-ses": "^3.515.0",
|
||||
"@inquirer/prompts": "^7.2.0",
|
||||
"@pretendonetwork/grpc": "^2.5.10",
|
||||
"@pretendonetwork/grpc": "^2.6.1",
|
||||
"bcrypt": "^5.0.0",
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"colors": "^1.4.0",
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import express from 'express';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
@@ -177,6 +177,8 @@ router.post('/', passwordResetRatelimit, async (request: express.Request, respon
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
response.json({
|
||||
app: 'api',
|
||||
status: 200
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
@@ -92,5 +92,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Empt
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const TOKEN_REQUIRED_PATHS = [
|
||||
'/api.v2.ApiService/GetUserData',
|
||||
'/api.v2.ApiService/UpdateUserData',
|
||||
'/api.v2.ApiService/ResetPassword', // * This paths token is not an authentication token, it is a password reset token
|
||||
'/api.v2.ApiService/UpdatePassword',
|
||||
'/api.v2.ApiService/SetDiscordConnectionData',
|
||||
'/api.v2.ApiService/SetStripeConnectionData',
|
||||
'/api.v2.ApiService/RemoveConnection',
|
||||
|
||||
@@ -5,6 +5,7 @@ import { updateUserData } from '@/services/grpc/api/v2/update-user-data';
|
||||
import { updateEmail } from '@/services/grpc/api/v2/update-email';
|
||||
import { verifyEmail } from '@/services/grpc/api/v2/verify-email';
|
||||
import { forgotPassword } from '@/services/grpc/api/v2/forgot-password';
|
||||
import { updatePassword } from '@/services/grpc/api/v2/update-password';
|
||||
import { resetPassword } from '@/services/grpc/api/v2/reset-password';
|
||||
import { setDiscordConnectionData } from '@/services/grpc/api/v2/set-discord-connection-data';
|
||||
import { setStripeConnectionData } from '@/services/grpc/api/v2/set-stripe-connection-data';
|
||||
@@ -18,6 +19,7 @@ export const apiServiceImplementationV2 = {
|
||||
updateEmail,
|
||||
verifyEmail,
|
||||
forgotPassword,
|
||||
updatePassword,
|
||||
resetPassword,
|
||||
setDiscordConnectionData,
|
||||
setStripeConnectionData,
|
||||
|
||||
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { PasswordResetToken } from '@/models/password-reset-token';
|
||||
import { nintendoPasswordHash } from '@/util';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import { getPNIDByPID } from '@/database';
|
||||
import { SystemType } from '@/types/common/system-types';
|
||||
import { TokenType } from '@/types/common/token-types';
|
||||
@@ -91,5 +91,7 @@ export async function resetPassword(request: ResetPasswordRequest): Promise<Rese
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
69
src/services/grpc/api/v2/update-password.ts
Normal file
69
src/services/grpc/api/v2/update-password.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import bcrypt from 'bcrypt';
|
||||
import { Status, ServerError } from 'nice-grpc';
|
||||
import { nintendoPasswordHash, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import type { CallContext } from 'nice-grpc';
|
||||
import type { UpdatePasswordRequest, UpdatePasswordResponse } from '@pretendonetwork/grpc/api/v2/update_password_rpc';
|
||||
import type { AuthenticationCallContextExt } from '@/services/grpc/api/v2/authentication-middleware';
|
||||
|
||||
// * 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/;
|
||||
|
||||
export async function updatePassword(request: UpdatePasswordRequest,
|
||||
context: CallContext & AuthenticationCallContextExt
|
||||
): Promise<UpdatePasswordResponse> {
|
||||
// * This is asserted in authentication-middleware, we know this is never null
|
||||
const pnid = context.pnid!;
|
||||
|
||||
const oldPassword = request.oldPassword.trim();
|
||||
const newPassword = request.newPassword.trim();
|
||||
const newPasswordConfirm = request.newPasswordConfirm.trim();
|
||||
|
||||
const hashedOldPassword = nintendoPasswordHash(oldPassword, pnid.pid);
|
||||
|
||||
if (!bcrypt.compareSync(hashedOldPassword, pnid.password)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password is incorrect');
|
||||
}
|
||||
|
||||
if (!newPassword) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Must enter a new password');
|
||||
}
|
||||
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Passwords do not match');
|
||||
}
|
||||
|
||||
if (newPassword === oldPassword) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'New password must not equal current password');
|
||||
}
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 16) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password must be between 6 and 16 characters long');
|
||||
}
|
||||
|
||||
if (newPassword.toLowerCase() === pnid.username.toLowerCase()) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password cannot be the same as username');
|
||||
}
|
||||
|
||||
if (!PASSWORD_WORD_OR_NUMBER_REGEX.test(newPassword) && !PASSWORD_WORD_OR_PUNCTUATION_REGEX.test(newPassword) && !PASSWORD_NUMBER_OR_PUNCTUATION_REGEX.test(newPassword)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password must have combination of letters, numbers, and/or punctuation characters');
|
||||
}
|
||||
|
||||
if (PASSWORD_REPEATED_CHARACTER_REGEX.test(newPassword)) {
|
||||
throw new ServerError(Status.INVALID_ARGUMENT, 'Password may not have 3 repeating characters');
|
||||
}
|
||||
|
||||
const primaryPasswordHash = nintendoPasswordHash(newPassword, pnid.pid);
|
||||
const passwordHash = await bcrypt.hash(primaryPasswordHash, 10);
|
||||
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await pnid.save();
|
||||
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import moment from 'moment';
|
||||
import deviceCertificateMiddleware from '@/middleware/device-certificate';
|
||||
import { deviceRatelimit } from '@/middleware/ratelimit';
|
||||
import { connection as databaseConnection, doesPNIDExist, getPNIDProfileJSONByPID } from '@/database';
|
||||
import { getAgeFromDate, getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail } from '@/util';
|
||||
import { getAgeFromDate, getValueFromHeaders, nintendoPasswordHash, sendConfirmationEmail, sendPNIDDeletedEmail, sendPasswordResetNoticeEmail } from '@/util';
|
||||
import IP2LocationManager from '@/ip2location';
|
||||
import { PNID } from '@/models/pnid';
|
||||
import { NEXAccount } from '@/models/nex-account';
|
||||
@@ -652,6 +652,7 @@ router.put('/@me', async (request: express.Request, response: express.Response):
|
||||
pnid.password = passwordHash;
|
||||
|
||||
await pnid.removeAllTokens();
|
||||
await sendPasswordResetNoticeEmail(pnid);
|
||||
}
|
||||
|
||||
pnid.gender = gender;
|
||||
|
||||
15
src/util.ts
15
src/util.ts
@@ -177,6 +177,21 @@ export async function sendEmailConfirmedEmail(pnid: mongoose.HydratedDocument<IP
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendPasswordResetNoticeEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const noticeEmail = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
.addParagraph('your password has been changed.')
|
||||
.addParagraph('If this wasn\'t you, contact [support@pretendo.network](mailto:support@pretendo.network).');
|
||||
|
||||
const noticeOptions = {
|
||||
to: pnid.email.address,
|
||||
subject: '[Pretendo Network] Password changed',
|
||||
email: noticeEmail
|
||||
};
|
||||
|
||||
await sendMail(noticeOptions);
|
||||
}
|
||||
|
||||
export async function sendEmailConfirmedParentalControlsEmail(pnid: mongoose.HydratedDocument<IPNID, IPNIDMethods>): Promise<void> {
|
||||
const email = new CreateEmail()
|
||||
.addHeader('Dear {{pnid}},', { pnid: pnid.username })
|
||||
|
||||
Reference in New Issue
Block a user