mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-12 09:18:25 +03:00
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user