summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2019-05-26 12:29:34 -0400
committergumi <git@gumi.ca>2019-05-26 12:29:34 -0400
commit0296991542f7642751b03dbff338fe0360387095 (patch)
tree2a4333e89ac38e253dfa9a0db595398100b3d92a
parent0744c3267918165c773ea9a26b29adcf4696dafe (diff)
downloadapi-0296991542f7642751b03dbff338fe0360387095.tar.gz
api-0296991542f7642751b03dbff338fe0360387095.tar.bz2
api-0296991542f7642751b03dbff338fe0360387095.tar.xz
api-0296991542f7642751b03dbff338fe0360387095.zip
add uuid-based password reset
-rw-r--r--package.json5
-rw-r--r--src/api.js1
-rw-r--r--src/routers/tmwa/middlewares/account.js119
3 files changed, 119 insertions, 6 deletions
diff --git a/package.json b/package.json
index 1e7ff74..0cfdbcb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tmw-api",
- "version": "2.1.0",
+ "version": "2.2.0",
"description": "TMW RESTful API",
"author": "The Mana World",
"license": "CC0-1.0",
@@ -14,7 +14,8 @@
"name": "The Mana World Legacy Server",
"url": "tmwa://server.themanaworld.org:6901",
"root": "/mnt/tmwAthena/tmwa-server-data",
- "home": "/home/tmw"
+ "home": "/home/tmw",
+ "reset": "https://www.themanaworld.org/recover/password/#"
},
"mailer": {
"from": "The Mana World <noreply@themanaworld.org>"
diff --git a/src/api.js b/src/api.js
index 449385e..b5c573b 100644
--- a/src/api.js
+++ b/src/api.js
@@ -123,6 +123,7 @@ const tmwa_router = new (require("./routers/tmwa"))({
url: process.env.npm_package_config_tmwa_url,
root: process.env.npm_package_config_tmwa_root,
home: process.env.npm_package_config_tmwa_home,
+ reset: process.env.npm_package_config_tmwa_reset,
}, api, checkCaptcha, checkRateLimiting);
global_router.use("/tmwa", tmwa_router);
diff --git a/src/routers/tmwa/middlewares/account.js b/src/routers/tmwa/middlewares/account.js
index d3c32b9..1f5ed42 100644
--- a/src/routers/tmwa/middlewares/account.js
+++ b/src/routers/tmwa/middlewares/account.js
@@ -169,13 +169,47 @@ const reset_password = async (req, res, next) => {
req.body.email.match(/^(?:[a-zA-Z0-9.$&+=_~-]{1,34}@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,35}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,34}[a-zA-Z0-9])?){0,9})$/) &&
req.body.email.length >= 3 && req.body.email.length < 40 &&
req.body.email !== "a@a.com") {
- // recover by email (currently unsupported)
- res.status(501).json({
- status: "error",
- error: "not yet implemented"
+
+ const accounts = await findAccountsByEmail(req.body.email);
+
+ if (accounts.size < 1) {
+ res.status(404).json({
+ status: "error",
+ error: "no accounts found"
+ });
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 8000);
+ return;
+ }
+
+ const uuid = uuidv4();
+ transporter.sendMail({
+ from: req.app.locals.mailer.from,
+ to: email,
+ subject: "The Mana World password reset",
+ text: "You are receiving this email because someone (you?) has requested a password reset on The Mana World"+
+ "with your email address.\nIf you did not request a password reset please ignore this email.\n\n"+
+ "The following accounts are associated with this email address:\n" + account_names + "\n\n"+
+ "To proceed with the password reset:\n" + `${req.app.locals.tmwa.reset}${uuid}`
+ }, (err, info) => {
+ pending_operations.set(uuid, {
+ type: "reset",
+ accounts: accounts,
+ initiated: Date.now(),
+ ip: req.ip,
+ timeout: setTimeout(() => pending_operations.delete(uuid), 3600000), // 60 minutes
+ });
+ res.status(200).json({
+ status: "success"
+ });
+ req.app.locals.logger.info(`TMWA.account: initiated password reset: ${info.messageId} [${req.ip}]`);
});
+
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 8000);
return;
} else if (req.body && Reflect.has(req.body, "username") &&
+ !Reflect.has(req.body, "password") &&
req.body.username.match(/^[a-zA-Z0-9]{4,23}$/)) {
// recover by username (currently unsupported)
res.status(501).json({
@@ -186,7 +220,9 @@ const reset_password = async (req, res, next) => {
}
if (!req.body || !Reflect.has(req.body, "password") ||
+ !Reflect.has(req.body, "username") ||
!Reflect.has(req.body, "code") ||
+ !req.body.username.match(/^[a-zA-Z0-9]{4,23}$/) ||
!req.body.password.match(/^[a-zA-Z0-9]{4,23}$/) ||
!req.body.code.match(/^[a-zA-Z0-9-_]{6,128}$/))
{
@@ -200,6 +236,81 @@ const reset_password = async (req, res, next) => {
}
// actual reset happens here
+ if (!pending_operations.has(req.body.code)) {
+ res.status(408).json({
+ status: "error",
+ error: "request expired"
+ });
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000);
+ return;
+ }
+
+ if (pending_operations.get(req.body.code).type !== "reset") {
+ res.status(400).json({
+ status: "error",
+ error: "invalid type"
+ });
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000);
+ pending_operations.delete(req.body.code);
+ req.app.locals.logger.warn(`TMWA.account: attempted reset account with invalid uuid: ${req.body.username} [${req.ip}]`);
+ return;
+ }
+
+ for (account of pending_operations.get(req.body.code).accounts) {
+ if (account.name === req.body.username) {
+ pending_operations.delete(req.body.code);
+ const child = execFile(`${req.app.locals.tmwa.home}/.local/bin/tmwa-admin`, [], {
+ cwd: `${req.app.locals.tmwa.root}/login/`,
+ env: {
+ LD_LIBRARY_PATH: `${req.app.locals.tmwa.home}/.local/lib`,
+ }
+ });
+
+ child.stdin.write(`password ${req.body.username} ${req.body.password}\n`);
+ child.stderr.on("data", data => {
+ console.error("TMWA.account: an unexpected tmwa-admin error occured: %s", data);
+ return;
+ });
+ child.stdout.on("data", data => {
+ if (!data.includes("successfully")) {
+ if (!data.includes("have a connection"))
+ console.error("TMWA.account: an unexpected tmwa-admin error occured: %s", data);
+ child.kill();
+ return;
+ }
+
+ res.status(201).json({
+ status: "success"
+ });
+ req.app.locals.logger.info(`TMWA.account: password has been reset: ${req.body.username} [${req.ip}]`);
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000);
+
+ transporter.sendMail({
+ from: req.app.locals.mailer.from,
+ to: email,
+ subject: "The Mana World password reset",
+ text: `You have successfully reset the password for account \"${req.body.username}\".\nHave fun playing The Mana World!`
+ }, (err, info) => {
+ req.app.locals.logger.info(`TMWA.account: sent password reset confirmation email: ${req.body.username} ${info.messageId}`);
+ });
+ });
+ child.stdin.end();
+ return;
+ }
+ }
+
+ res.status(401).json({
+ status: "error",
+ error: "foreign account"
+ });
+ req.app.locals.rate_limiting.add(req.ip);
+ setTimeout(() => req.app.locals.rate_limiting.delete(req.ip), 300000);
+ pending_operations.delete(req.body.code);
+ req.app.locals.logger.warn(`TMWA.account: attempted reset account not owned by user: ${req.body.username} [${req.ip}]`);
+ return;
};