summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--server.js121
-rw-r--r--tools/sql/schema.txt6
-rw-r--r--views/forgot_password.ejs13
-rw-r--r--views/login.ejs2
-rw-r--r--views/reset_password.ejs14
5 files changed, 152 insertions, 4 deletions
diff --git a/server.js b/server.js
index 327fa59..f5eb099 100644
--- a/server.js
+++ b/server.js
@@ -357,9 +357,114 @@ app.get('/unsubscribe', must_be_logged_in, function (req, res) {
res.redirect('/profile');
});
+/*
+ * FORGOT AND CHANGE PASSWORD
+ */
+
+const sql_select_salt = db.prepare("SELECT salt FROM users WHERE user_id = ?").pluck();
+const sql_find_user_by_mail = db.prepare("SELECT user_id FROM users WHERE mail = ?").pluck();
+
+const sql_find_forgot_password_token = db.prepare(`
+ SELECT token FROM forgot_password WHERE user_id = ? AND datetime('now') < datetime(time, '+5 minutes')
+ `).pluck();
+const sql_verify_forgot_password_token = db.prepare(`
+ SELECT COUNT(*) FROM forgot_password WHERE user_id = ? AND datetime('now') < datetime(time, '+20 minutes') AND token = ?
+ `).pluck();
+const sql_create_forgot_password_token = db.prepare(`
+ INSERT OR REPLACE INTO forgot_password VALUES ( ?, lower(hex(randomblob(16))), datetime('now') )
+ `);
+
+app.get('/forgot_password', function (req, res) {
+ LOG(req, "GET /forgot_password");
+ res.render('forgot_password.ejs', { user: req.user, message: req.flash('message') });
+});
+
+app.get('/reset_password', function (req, res) {
+ LOG(req, "GET /reset_password");
+ res.render('reset_password.ejs', { user: null, mail: "", token: "", message: req.flash('message') });
+});
+
+app.get('/reset_password/:mail', function (req, res) {
+ let mail = req.params.mail;
+ LOG(req, "GET /reset_password", mail);
+ res.render('reset_password.ejs', { user: null, mail: mail, token: "", message: req.flash('message') });
+});
+
+app.get('/reset_password/:mail/:token', function (req, res) {
+ let mail = req.params.mail;
+ let token = req.params.token;
+ LOG(req, "GET /reset_password", mail, token);
+ res.render('reset_password.ejs', { user: null, mail: mail, token: token, message: req.flash('message') });
+});
+
+app.post('/forgot_password', function (req, res) {
+ LOG(req, "POST /forgot_password");
+ try {
+ if (sql_blacklist_ip.get(req.connection.remoteAddress)[0] != 0)
+ return res.redirect('/banned');
+ let mail = req.body.mail;
+ let user_id = sql_find_user_by_mail.get(mail);
+ if (user_id) {
+ let token = sql_find_forgot_password_token.get(user_id);
+ if (!token) {
+ sql_create_forgot_password_token.run(user_id);
+ token = sql_find_forgot_password_token.get(user_id);
+ console.log("FORGOT - create and mail token", token);
+ mail_password_reset_token(mail, token);
+ } else {
+ console.log("FORGOT - existing token - ignore request", token);
+ }
+ req.flash('message', "A password reset token has been sent to " + mail + ".");
+ if (is_email(mail))
+ return res.redirect('/reset_password/' + mail);
+ return res.redirect('/reset_password/');
+ }
+ req.flash('message', "User not found.");
+ return res.redirect('/forgot_password');
+ } catch (err) {
+ console.log(err);
+ req.flash('message', err.message);
+ return res.redirect('/forgot_password');
+ }
+});
+
+app.post('/reset_password', function (req, res) {
+ let mail = req.body.mail;
+ let token = req.body.token;
+ let password = req.body.password;
+ try {
+ LOG(req, "POST /reset_password", mail, token);
+ let user_id = sql_find_user_by_mail.get(mail);
+ if (!user_id) {
+ req.flash('message', "User not found.");
+ return res.redirect('/reset_password/'+mail+'/'+token);
+ }
+ if (password.length < 4) {
+ req.flash('message', "Password is too short!");
+ return res.redirect('/reset_password/'+mail+'/'+token);
+ }
+ if (!sql_verify_forgot_password_token.get(user_id, token)) {
+ req.flash('message', "Invalid or expired token!");
+ return res.redirect('/reset_password/'+mail);
+ }
+ let salt = sql_select_salt.get(user_id);
+ if (!salt) {
+ req.flash('message', "User not found.");
+ return res.redirect('/reset_password/'+mail+'/'+token);
+ }
+ let hash = hash_password(password, salt);
+ db.prepare("UPDATE users SET password = ? WHERE user_id = ?").run(hash, user_id);
+ req.flash('message', "Your password has been updated.");
+ return res.redirect('/login');
+ } catch (err) {
+ console.log(err);
+ req.flash('message', err.message);
+ return res.redirect('/reset_password/'+mail+'/'+token);
+ }
+});
+
app.post('/change_password', must_be_logged_in, function (req, res) {
try {
- let name = clean_user_name(req.user.name);
let password = req.body.password;
let newpass = req.body.newpass;
LOG(req, "POST /change_password", name);
@@ -367,12 +472,11 @@ app.post('/change_password', must_be_logged_in, function (req, res) {
req.flash('message', "Password is too short!");
return res.redirect('/change_password');
}
- let salt_row = db.prepare("SELECT salt FROM users WHERE name = ?").get(name);
- if (!salt_row) {
+ let salt = sql_select_salt.get(req.user.user_id);
+ if (!salt) {
req.flash('message', "User not found.");
return res.redirect('/change_password');
}
- let salt = salt_row.salt;
let hash = hash_password(password, salt);
let user_row = db.prepare("SELECT user_id, name FROM users WHERE name = ? AND password = ?").get(name, hash);
if (!user_row) {
@@ -381,6 +485,7 @@ app.post('/change_password', must_be_logged_in, function (req, res) {
}
hash = hash_password(newpass, salt);
db.prepare("UPDATE users SET password = ? WHERE user_id = ?").run(hash, user_row.user_id);
+ req.flash('message', "Your password has been updated.");
return res.redirect('/profile');
} catch (err) {
console.log(err);
@@ -900,6 +1005,14 @@ function mail_callback(err, info) {
console.log("MAIL SENT", err, info);
}
+function mail_password_reset_token(mail, token) {
+ let subject = "Rally the Troops - Password reset request";
+ let body = "Your password reset token is: " + token + "\n\n";
+ body += "https://rally-the-troops.com/reset_password/" + mail + "/" + token + "\n\n"
+ body += "If you did not request a password reset you can ignore this mail.\n\n";
+ mailer.sendMail({ from: MAIL_FROM, to: mail, subject: subject, text: body }, mail_callback);
+}
+
function mail_your_turn_notification(user, game_id, interval) {
let too_soon = sql_notify_too_soon.get(interval, user.user_id, game_id);
console.log("YOUR TURN (OFFLINE):", game_id, user.name, user.mail, too_soon);
diff --git a/tools/sql/schema.txt b/tools/sql/schema.txt
index a75ce1d..bd695f7 100644
--- a/tools/sql/schema.txt
+++ b/tools/sql/schema.txt
@@ -18,6 +18,12 @@ CREATE TABLE IF NOT EXISTS notifications (
UNIQUE ( user_id, game_id )
);
+CREATE TABLE IF NOT EXISTS forgot_password (
+ user_id INTEGER PRIMARY KEY,
+ token TEXT,
+ time TIMESTAMP
+);
+
CREATE TABLE IF NOT EXISTS blacklist_ip ( ip TEXT PRIMARY KEY );
CREATE TABLE IF NOT EXISTS blacklist_mail ( mail TEXT PRIMARY KEY );
diff --git a/views/forgot_password.ejs b/views/forgot_password.ejs
new file mode 100644
index 0000000..1679496
--- /dev/null
+++ b/views/forgot_password.ejs
@@ -0,0 +1,13 @@
+<%- include('header', { title: "Forgot password" }) %>
+<% if (user) { %>
+<p>
+You're already logged in!
+<% } else { %>
+<form action="/forgot_password" method="post">
+<p>
+<label for="mail">Mail: </label><br>
+<input type="mail" id="mail" name="mail" required>
+<p>
+<button type="submit">Forgot password</button>
+</form>
+<% } %>
diff --git a/views/login.ejs b/views/login.ejs
index a5e2546..b4089fc 100644
--- a/views/login.ejs
+++ b/views/login.ejs
@@ -12,4 +12,6 @@
<p>
<button type="submit">Login</button>
</form>
+<p>
+<a href="/forgot_password">Forgot password</a>
<% } %>
diff --git a/views/reset_password.ejs b/views/reset_password.ejs
new file mode 100644
index 0000000..8920da7
--- /dev/null
+++ b/views/reset_password.ejs
@@ -0,0 +1,14 @@
+<%- include('header', { title: "Reset password" }) %>
+<form action="/reset_password" method="post">
+<p>
+<label for="mail">Mail: </label><br>
+<input type="text" id="mail" name="mail" size="32" value="<%= mail %>" required>
+<p>
+<label for="password">New Password: </label><br>
+<input type="password" id="password" name="password" size="32" required>
+<p>
+<label for="token">Token: </label><br>
+<input type="text" id="token" name="token" size="32" value="<%= token %>" style="font-family:monospace" required>
+<p>
+<button type="submit">Reset password</button>
+</form>