diff --git a/.gitignore b/.gitignore
index 1bd7226..2cc42ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
+db/*.db
node_modules
*.swp
diff --git a/client/favicon.xcf b/client/favicon.xcf
new file mode 100644
index 0000000..d4b65f7
Binary files /dev/null and b/client/favicon.xcf differ
diff --git a/client/public/assets/favicon-128.png b/client/public/assets/favicon-128.png
new file mode 100644
index 0000000..e38e665
Binary files /dev/null and b/client/public/assets/favicon-128.png differ
diff --git a/client/public/assets/favicon-152.png b/client/public/assets/favicon-152.png
new file mode 100644
index 0000000..0e06562
Binary files /dev/null and b/client/public/assets/favicon-152.png differ
diff --git a/client/public/assets/favicon-167.png b/client/public/assets/favicon-167.png
new file mode 100644
index 0000000..f38da3d
Binary files /dev/null and b/client/public/assets/favicon-167.png differ
diff --git a/client/public/assets/favicon-180.png b/client/public/assets/favicon-180.png
new file mode 100644
index 0000000..f21e31b
Binary files /dev/null and b/client/public/assets/favicon-180.png differ
diff --git a/client/public/assets/favicon-192.png b/client/public/assets/favicon-192.png
new file mode 100644
index 0000000..ddb51da
Binary files /dev/null and b/client/public/assets/favicon-192.png differ
diff --git a/client/public/assets/favicon-256.png b/client/public/assets/favicon-256.png
new file mode 100644
index 0000000..0b32f4b
Binary files /dev/null and b/client/public/assets/favicon-256.png differ
diff --git a/client/public/assets/favicon-32.png b/client/public/assets/favicon-32.png
new file mode 100644
index 0000000..176b7a5
Binary files /dev/null and b/client/public/assets/favicon-32.png differ
diff --git a/client/src/App.js b/client/src/App.js
index 83be7bc..44f8f07 100644
--- a/client/src/App.js
+++ b/client/src/App.js
@@ -16,6 +16,8 @@ import { GlobalContext } from "./GlobalContext.js";
import SignIn from "./SignIn.js";
import SignUp from "./SignUp.js";
import Group from "./Group.js";
+import VerifyEmail from "./VerifyEmail.js";
+import Dashboard from "./Dashboard.js";
import { base } from "./Common.js";
@@ -46,23 +48,21 @@ const App = () => {
To do so, simply access this link:
", - "", + "", "", "Sincerely,
", "James
" ].join("\n"), "text": [ - "Hello {{username}},", + "Hello {{firstName}},", "", "Welcome to goodtimes.ketrenos.com. You are almost done creating your account. ", - "Before you can access the system, you must verify your email address.", + "Before you can access the system, you must verify your email address {{email}}.", "", "To do so, simply access this link:", "", @@ -32,19 +40,35 @@ const templates = { "James" ].join("\n") }, - "password": { + "verified": { "html": [ - "Hello {{username}},
", - "", - "You changed your password on ketrenos.com.
", + "The user {{email}} verified their address and is now active", + "on Goodtimes.
", "", "Sincerely,
", "James
" ].join("\n"), "text": [ - "Hello {{username}},", + "The user {{email}} verified their address and is now active", + "on Goodtimes.", "", - "You changed your password on ketrenos.com.", + "Sincerely,", + "James" + ].join("\n") + }, + "password": { + "html": [ + "Hello {{firstName}},
", + "", + "You changed your password on goodtimes.ketrenos.com.
", + "", + "Sincerely,
", + "James
" + ].join("\n"), + "text": [ + "Hello {{firstName}},", + "", + "You changed your password on goodtimes.ketrenos.com.", "", "Sincerely,", "James" @@ -82,21 +106,21 @@ const sendVerifyMail = function(userDB, req, user) { throw error; }); }).then(function(secret) { - const transporter = req.app.get("transporter"); if (!transporter) { console.log("Not sending VERIFY email; SMTP not configured."); return; } let data = { - username: user.displayName, - mail: user.mail, + firstName: user.firstName, + email: user.email, secret: secret, - url: req.protocol + "://" + req.hostname + req.app.get("basePath") + url: req.protocol + "://" + req.hostname + req.app.get("basePath") + + "/user/verify-email/" }, envelope = { - to: data.mail, + to: data.email, from: config.get("smtp.sender"), - subject: "Request to ketrenos.com create account for '" + data.username + "'", + subject: "Request to goodtimes.ketrenos.com create account for '" + data.firstName + "'", cc: "", bcc: config.get("admin.mail"), text: hb.compile(templates.verify.text)(data), @@ -119,7 +143,7 @@ const sendVerifyMail = function(userDB, req, user) { } attempts--; - console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error); + console.log("Unable to send email. Trying again in 100ms (" + attempts + " attempts remain): ", error); setTimeout(send.bind(undefined, envelope), 100); }); } @@ -132,20 +156,19 @@ const sendVerifyMail = function(userDB, req, user) { }; const sendPasswordChangedMail = function(userDB, req, user) { - const transporter = req.app.get("transporter"); if (!transporter) { console.log("Not sending VERIFY email; SMTP not configured."); return; } let data = { - username: user.displayName, - mail: user.mail, + firstName: user.firstName, + email: user.email, url: req.protocol + "://" + req.hostname + req.app.get("basePath") }, envelope = { - to: data.mail, + to: data.email, from: config.get("smtp.sender"), - subject: "Password changed on ketrenos.com for '" + data.username + "'", + subject: "Password changed on goodtimes.ketrenos.com for '" + data.firstName + "'", cc: "", bcc: config.get("admin.mail"), text: hb.compile(templates.password.text)(data), @@ -168,7 +191,50 @@ const sendPasswordChangedMail = function(userDB, req, user) { } attempts--; - console.log("Unable to send mail. Trying again in 100ms (" + attempts + " attempts remain): ", error); + console.log("Unable to send email. Trying again in 100ms (" + attempts + " attempts remain): ", error); + setTimeout(send.bind(undefined, envelope), 100); + }); + } + + send(envelope); + }); +}; + +const sendVerifiedMail = function (userDB, req, user) { + if (!transporter) { + console.log("Not sending VERIFIED email; SMTP not configured."); + return; + } + + const envelope = { + to: config.get("admin.mail"), + from: config.get("smtp.sender"), + subject: "VERIFIED: Account'" + user.email + "'", + cc: "", + bcc: "", + text: hb.compile(templates.verified.text)(user), + html: hb.compile(templates.verified.html)(user) + }; + + return new Promise(function (resolve, reject) { + let attempts = 10; + + function send(envelope) { + /* Rate limit to ten per second */ + transporter.sendMail(envelope, function (error, info) { + if (!error) { + console.log('Message sent: ' + info.response); + return resolve(); + } + + if (attempts == 0) { + console.log("Error sending email: ", error) + return reject(error); + } + + attempts--; + console.log("Unable to send mail. Trying again in 100ms (" + + attempts + " attempts remain): ", error); setTimeout(send.bind(undefined, envelope), 100); }); } @@ -179,5 +245,6 @@ const sendPasswordChangedMail = function(userDB, req, user) { module.exports = { sendVerifyMail, + sendVerifiedMail, sendPasswordChangedMail } diff --git a/server/routes/groups.js b/server/routes/groups.js index 6475c18..764fdc9 100755 --- a/server/routes/groups.js +++ b/server/routes/groups.js @@ -38,7 +38,19 @@ router.get("/", (req, res/*, next*/) => { return res.status(200).send({ user: userId }); }); -router.post("/:id?", async (req, res/*, next*/) => { +router.post("/", async (req, res/*, next*/) => { + console.log(`POST /groups/`, req.session.userId); + return res.status(200).send( + [ { + id: 1, + ownerId: 1, + name: "Beer Tuesday", + nextEvent: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */ + } ] + ); +}); + +router.post("/:id", async (req, res/*, next*/) => { const { id } = req.params; let userId; diff --git a/server/routes/users.js b/server/routes/users.js index 35973d7..04df529 100755 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -2,10 +2,11 @@ const express = require("express"), config = require("config"), - { sendVerifyMail, sendPasswordChangedMail } = require("../lib/mail"), + { sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } = require("../lib/mail"), crypto = require("crypto"); const router = express.Router(); +const autoAuthenticate = 1; let userDB; @@ -82,57 +83,75 @@ router.get("/csrf", (req, res) => { res.json({ csrfToken: req.csrfToken() }); }); -router.post("/create", function(req, res) { - console.log("/users/create"); +router.post("/signup", function(req, res) { + console.log("/users/signup"); const user = { - uid: req.query.m || req.body.m, - displayName: req.query.n || req.body.n || "", - password: req.query.p || req.body.p || "", - mail: req.query.m || req.body.m, - notes: req.query.w || req.body.w || "" + uid: req.body.email, + familyName: req.body.familyName, + firstName: req.body.firstName, + password: req.body.password, + email: req.body.email, }; - if (!user.uid || !user.password || !user.displayName || !user.notes) { - return res.status(400).send("Missing email address, password, name, and/or who you know."); + if (!user.uid + || !user.email + || !user.password + || !user.familyName + || !user.firstName) { + return res.status(400).send({ + message: `Missing email address, password, and/or name.` + }); } user.password = crypto.createHash('sha256') .update(user.password).digest('base64'); user.md5 = crypto.createHash('md5') - .update(data).digest('base64'); + .update(user.email).digest('base64'); return userDB.sequelize.query("SELECT * FROM users WHERE uid=:uid", { replacements: user, type: userDB.Sequelize.QueryTypes.SELECT, raw: true - }).then(function(results) { - if (results.length != 0) { - return res.status(400).send("Email address already used."); + }).then(async function(results) { + if (results.length != 0 && results[0].mailVerified) { + return res.status(400).send({ + message: `Email address already used.` + }); } let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; - if (!re.exec(user.mail)) { - console.log("Invalid email address: " + user.mail); - throw "Invalid email address."; + if (!re.exec(user.email)) { + const error = `Invalid email address: ${user.email}.`; + console.log(error); + return res.status(401).send({ + message: error + }); } - }).then(function() { - return userDB.sequelize.query("INSERT INTO users " + - "(uid,displayName,password,mail,memberSince,authenticated,notes,md5) " + - "VALUES(:uid,:displayName,:password,:mail,CURRENT_TIMESTAMP,0,:notes,:md5)", { - replacements: user - }).spread(function(results, metadata) { - req.session.userId = metadata.lastID; - }).then(function() { + + try { + if (results.length != 0) { + await userDB.sequelize.query("UPDATE users " + + "SET mailVerified=0"); + req.session.userId = results[0].id; + } else { + let [, metadata] = await userDB.sequelize.query("INSERT INTO users " + + "(uid,firstName,familyName,password,email,memberSince," + + "authenticated,md5) " + + `VALUES(:uid,:firstName,:familyName,:password,` + + `:email,CURRENT_TIMESTAMP,${autoAuthenticate},:md5)`, { + replacements: user + }); + req.session.userId = metadata.lastID; + } return getSessionUser(req).then(function(user) { res.status(200).send(user); user.id = req.session.userId; return sendVerifyMail(userDB, req, user); }); - }).catch(function(error) { - console.log("Error creating account: ", error); - return res.status(401).send(error); - }); + } catch (error) { + console.error(error); + } }); }); @@ -143,7 +162,7 @@ const getSessionUser = function(req) { } let query = "SELECT " + - "uid AS username,displayName,mailVerified,authenticated,memberSince AS name,mail " + + "uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 " + "FROM users WHERE id=:id"; return userDB.sequelize.query(query, { replacements: { @@ -173,7 +192,7 @@ const getSessionUser = function(req) { }).then(function(user) { /* Strip out any fields that shouldn't be there. The allowed fields are: */ let allowed = [ - "maintainer", "username", "displayName", "mailVerified", "authenticated", "name", "mail", "restriction" + "maintainer", "username", "firstName", "familyName", "mailVerified", "authenticated", "name", "email", "restriction", "md5" ]; for (let field in user) { if (allowed.indexOf(field) == -1) { @@ -184,13 +203,70 @@ const getSessionUser = function(req) { }); } -router.post("/login", function(req, res) { - console.log("/users/login"); +router.post("/verify-email", async (req, res) => { + console.log("/users/verify-email"); + const key = req.body.token; + + let results = await userDB.sequelize.query( + "SELECT * FROM authentications WHERE key=:key", { + replacements: { key }, + type: userDB.sequelize.QueryTypes.SELECT + }); + + let token; + if (results.length == 0) { + console.log("Invalid key. Ignoring."); + return res.status(400).send({ + message: `Invalid authentication token.` + }); + } + + token = results[0]; +console.log(token); + console.log("Matched token: " + JSON.stringify(token, null, 2)); + switch (token.type) { + case "account-setup": + return userDB.sequelize.query( + "UPDATE users SET mailVerified=1 WHERE id=:userId", { + replacements: token + }) + .then(function () { + return userDB.sequelize.query( + "DELETE FROM authentications WHERE key=:key", { + replacements: { key } + }); + }) + .then(function () { + return userDB.sequelize.query( + "SELECT * FROM users WHERE id=:userId", { + replacements: token, + type: userDB.sequelize.QueryTypes.SELECT + }); + }) + .then(function (results) { + if (results.length == 0) { + return res.status(500).send({ + message: `Internal authentication error.` + }); + } + return results[0]; + }) + .then((user) => { + sendVerifiedMail(userDB, req, user); + req.session.userId = user.id; + }).then(function (user) { + return getSessionUser(req).then(function (user) { + return res.status(200).send(user); + }); + }); + } +}); + +router.post("/signin", function(req, res) { + console.log("/users/signin"); let { email, password } = req.body; - console.log("Login attempt"); - if (!email || !password) { return res.status(400).send({ message: `Missing email and/or password` @@ -202,7 +278,7 @@ router.post("/login", function(req, res) { let query = "SELECT " + "id,mailVerified,authenticated," + "uid AS username," + - "displayName AS name,mail " + + "familyName,firstName,email " + "FROM users WHERE uid=:username AND password=:password"; return userDB.sequelize.query(query, { replacements: { @@ -224,11 +300,11 @@ router.post("/login", function(req, res) { console.log(email + " not found (or invalid password.)"); req.session.userId = null; return res.status(401).send({ - message: `Invalid login credentials` + message: `Invalid sign in credentials` }); } - let message = "Logged in as " + user.displayName + " (" + user.id + ")"; + let message = "Logged in as " + user.email + " (" + user.id + ")"; if (!user.mailVerified) { console.log(message + ", who is not verified email."); } else if (!user.authenticated) { @@ -247,9 +323,8 @@ router.post("/login", function(req, res) { }); }); -router.get("/logout", function(req, res) { - console.log("/users/logout"); - +router.post("/signout", (req, res) => { + console.log("/users/signout"); if (req.session && req.session.userId) { req.session.userId = null; }