diff --git a/package-lock.json b/package-lock.json index 6cd8aa6..502f626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5317,6 +5317,16 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, + "query-string": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.5.0.tgz", + "integrity": "sha512-TYC4hDjZSvVxLMEucDMySkuAS9UIzSbAiYGyA9GWCjLKB8fQpviFbjd20fD7uejCDxZS+ftSdBKE6DS+xucJFg==", + "requires": { + "decode-uri-component": "^0.2.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + } + }, "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", @@ -6088,6 +6098,11 @@ "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" }, + "split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==" + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", @@ -6173,6 +6188,11 @@ "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, + "strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha1-ucczDHBChi9rFC3CdLvMWGbONUY=" + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", diff --git a/package.json b/package.json index e9790e7..f7865f5 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "forever": "^1.0.0", "formidable": "^1.2.1", "mysql": "^2.17.1", + "query-string": "^6.5.0", "react": "^16.8.6", "react-dom": "^16.8.6", "react-redux": "^7.0.3", diff --git a/server/accounts.js b/server/accounts.js index a7b6fdc..dd8b01f 100644 --- a/server/accounts.js +++ b/server/accounts.js @@ -265,10 +265,131 @@ function passwordChange(connection) { } } +function passwordRecover(connection) { + return (req, res) => { + //formidable handles forms + let form = formidable.IncomingForm(); + + //parse form + form.parse(req, (err, fields) => { + if (err) throw err; + + //validate email, username and password + if (!validateEmail(fields.email)) { + res.status(400).write('Invalid recover data'); + res.end(); + return; + } + + //ensure that this email is registered to an account + let query = 'SELECT accounts.id FROM accounts WHERE email = ?;'; + connection.query(query, [fields.email], (err, results) => { + if (err) throw err; + + if (results.length !== 1) { + res.status(400).write('Invalid recover data (did you use a registered email?)'); + res.end(); + return; + } + + //create the new recover record + let rand = Math.floor(Math.random() * 100000); + + let query = 'REPLACE INTO passwordRecover (accountId, token) VALUES (?, ?)'; + connection.query(query, [results[0].id, rand], (err) => { + if (err) throw err; + + //build the recovery email + let addr = `http://${process.env.WEB_ADDRESS}/passwordreset?email=${fields.email}&token=${rand}`; + let msg = 'Hello! Please visit the following address to set a new password (if you didn\'t request a password recovery, ignore this email): '; + let msgHtml = `
${msg}${addr}
`; + + //send the verification email + sendmail({ + from: `passwordrecover@${process.env.WEB_ADDRESS}`, + to: fields.email, + subject: 'Password Recovery', + text: msg + addr, + html: msgHtml + }, (err, reply) => { + //final check + if (err) { + res.write(`Something went wrong (did you use a valid email?)
${err}`) + res.end(); + return; + } + + res.status(200).write('Recovery email sent!'); + res.end(); + }); + }); + }); + }); + } +} + +function passwordReset(connection) { + return (req, res) => { + //formidable handles forms + let form = formidable.IncomingForm(); + + //parse form + form.parse(req, (err, fields) => { + if (err) throw err; + + //validate email, username and password + if (!validateEmail(fields.email) || fields.password.length < 8 || fields.password !== fields.retype) { + res.status(400).write('Invalid reset data (invalid email/password)'); + res.end(); + return; + } + + //get the account based on this email, token + let query = 'SELECT * FROM accounts WHERE email = ? AND id IN (SELECT passwordRecover.accountId FROM passwordRecover WHERE token = ?);'; + connection.query(query, [fields.email, fields.token], (err, results) => { + if (err) throw err; + + //results should be only 1 account + if (results.length !== 1) { + res.status(400).write('Invalid reset data (incorrect parameters/database state)'); + res.end(); + return; + } + + //generate the new salt, hash + bcrypt.genSalt(11, (err, salt) => { + if (err) throw err; + bcrypt.hash(fields.password, salt, (err, hash) => { + if (err) throw err; + + //update the salt, hash + let query = 'UPDATE accounts SET salt = ?, hash = ? WHERE email = ?;'; + connection.query(query, [salt, hash, fields.email], (err) => { + if (err) throw err; + + //delete the recover request from the database + let query = 'DELETE FROM passwordRecover WHERE accountId IN (SELECT id FROM accounts WHERE email = ?);'; + connection.query(query, [fields.email], (err) => { + if (err) throw err; + + res.status(200).write('Password updated!'); + res.end(); + return; + }); + }); + }); + }); + }); + }); + } +} + module.exports = { signup: signup, verify: verify, login: login, logout: logout, - passwordChange: passwordChange + passwordChange: passwordChange, + passwordRecover: passwordRecover, + passwordReset: passwordReset }; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 598bb22..18ef27a 100644 --- a/server/index.js +++ b/server/index.js @@ -21,6 +21,8 @@ app.get('/verify', accounts.verify(connection)); app.post('/login', accounts.login(connection)); app.post('/logout', accounts.logout(connection)); app.post('/passwordchange', accounts.passwordChange(connection)); +app.post('/passwordrecover', accounts.passwordRecover(connection)); +app.post('/passwordreset', accounts.passwordReset(connection)); //static directories app.use('/styles', express.static(path.resolve(__dirname + '/../public/styles')) ); diff --git a/sql/create_database_structure.sql b/sql/create_database_structure.sql index b7f4899..417253e 100644 --- a/sql/create_database_structure.sql +++ b/sql/create_database_structure.sql @@ -27,3 +27,12 @@ CREATE TABLE IF NOT EXISTS sessions ( CONSTRAINT FOREIGN KEY fk_accountId(accountId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE ); +CREATE TABLE IF NOT EXISTS passwordRecover ( + id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE, + td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(), + + accountId INTEGER UNSIGNED UNIQUE, + token INTEGER DEFAULT 0, + + CONSTRAINT FOREIGN KEY fk_accountId(accountId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/components/app.jsx b/src/components/app.jsx index b8cbfad..f40d848 100644 --- a/src/components/app.jsx +++ b/src/components/app.jsx @@ -3,6 +3,7 @@ import { BrowserRouter, Switch, Route } from 'react-router-dom'; //include pages import Home from './pages/home.jsx'; +import PasswordReset from './pages/password_reset.jsx' import PageNotFound from './pages/page_not_found.jsx'; //other stuff @@ -20,6 +21,7 @@ export default class App extends React.Component {{this.state.signupMsg}
-{this.state.recoverMsg}
+ ); + } + } + + return ( +{this.state.resetMsg}
); + } + } + + return ( +{this.state.warning}
+{this.state.warning}
+