Merge pull request #470 from TNG-dev:zkldi/issue-467-Users-should-be-able-to-change-their-own-password-while-logged-in

Change Password API.
This commit is contained in:
zkldi
2021-11-24 06:50:37 +00:00
committed by GitHub
3 changed files with 171 additions and 0 deletions
+19
View File
@@ -8,6 +8,19 @@
"@typescript-eslint",
"import"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"parserOptions": {
"project": [
"./tsconfig.json"
]
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
@@ -37,6 +50,12 @@
],
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-misused-promises": [
"error",
{
"checksVoidReturn": false
}
],
"@typescript-eslint/no-unused-vars": [
"warn",
{
@@ -4,6 +4,7 @@ import t from "tap";
import { CreateFakeAuthCookie } from "test-utils/fake-auth";
import mockApi from "test-utils/mock-api";
import ResetDBState from "test-utils/resets";
import { HashPassword, PasswordCompare } from "../../auth/auth";
t.test("GET /api/v1/users/:userID", (t) => {
t.beforeEach(ResetDBState);
@@ -232,6 +233,79 @@ t.test("PATCH /api/v1/users/:userID", async (t) => {
t.end();
});
t.test("POST /api/v1/users/:userID/change-password", async (t) => {
t.beforeEach(ResetDBState);
const cookie = await CreateFakeAuthCookie(mockApi);
function send(content: Record<string, unknown>) {
return mockApi.post("/api/v1/users/1/change-password").set("Cookie", cookie).send(content);
}
t.test("Must require !password and !oldPassword to be a valid password.", async (t) => {
const res = await send({
"!password": null,
"!oldPassword": "heres_my_old_password",
});
t.equal(res.statusCode, 400);
t.match(
res.body.description,
/!password/gu,
"Should be related to the invalid password field."
);
const res2 = await send({
"!password": "heres_my_new_password",
"!oldPassword": "short",
});
t.equal(res2.statusCode, 400);
t.match(
res2.body.description,
/!oldPassword/gu,
"Should be related to the invalid !oldPassword field."
);
t.end();
});
t.test("Must require !oldPassword to match.", async (t) => {
const res = await send({
"!password": "new_password",
"!oldPassword": "NOT_MY_PASSWORD",
});
t.equal(res.statusCode, 401);
t.match(res.body.description, /old password doesn't match/iu);
t.end();
});
t.test("Should update password if everything checks out.", async (t) => {
const res = await send({
"!password": "NEW_PASSWORD",
"!oldPassword": "password",
});
t.equal(res.statusCode, 200);
const newPrivateInfo = await db["user-private-information"].findOne({
userID: 1,
});
t.not(newPrivateInfo, null, "newPrivateInfo should not be null.");
t.ok(
PasswordCompare("NEW_PASSWORD", newPrivateInfo!.password),
"Should update the users password to the hash of NEW_PASSWORD."
);
t.end();
});
t.end();
});
t.test("GET /api/v1/users/:userID/game-stats", (t) => {
t.beforeEach(ResetDBState);
@@ -13,6 +13,7 @@ import {
GetRecentSessions,
} from "utils/queries/summary";
import { FormatUserDoc, GetAllRankings, GetUserWithID } from "utils/user";
import { HashPassword, PasswordCompare, ValidatePassword } from "../../auth/auth";
import apiTokensRouter from "./api-tokens/router";
import bannerRouter from "./banner/router";
import gamePTRouter from "./games/_game/_playtype/router";
@@ -263,6 +264,83 @@ router.get("/is-email-verified", RequireSelfRequestFromUser, async (req, res) =>
});
});
/**
* Changes the users password.
* Requires self-key level permissions.
*
* @param !password - The new password. Must pass password validation rules.
* @param !oldPassword - The old password.
*
* @name POST /api/v1/users/:userID/change-password
*/
router.post(
"/change-password",
RequireSelfRequestFromUser,
prValidate({
"!password": ValidatePassword,
"!oldPassword": ValidatePassword,
}),
async (req, res) => {
const user = req.session.tachi?.user;
/* istanbul ignore next */
if (!user) {
logger.severe(
`IP ${req.ip} got to /change-password without a user, but passed RequireSelfRequest?`
);
// this should be a 500, but lie to them.
return res.status(403).json({
success: false,
description: `You are not authorised to perform this action.`,
});
}
const privateInfo = await db["user-private-information"].findOne({
userID: user.id,
});
/* istanbul ignore next */
if (!privateInfo) {
logger.severe(`User ${FormatUserDoc(user)} has no private information?`, { user });
return res.status(500).json({
success: false,
description: `An internal server error has occured.`,
});
}
const isLastPasswordValid = await PasswordCompare(
req.body["!oldPassword"],
privateInfo.password
);
if (!isLastPasswordValid) {
return res.status(401).json({
success: false,
description: `Old Password doesn't match what we have in our records.`,
});
}
const newPasswordHash = await HashPassword(req.body["!password"]);
await db["user-private-information"].update(
{
userID: user.id,
},
{
$set: {
password: newPasswordHash,
},
}
);
return res.status(200).json({
success: true,
description: `Updated Password.`,
body: {},
});
}
);
router.use("/games/:game/:playtype", gamePTRouter);
router.use("/pfp", pfpRouter);
router.use("/banner", bannerRouter);