1
0

Authentication working

Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
This commit is contained in:
James Ketrenos 2022-04-06 17:33:18 -07:00
parent b6a62c7de2
commit 74f1f092ec
26 changed files with 557 additions and 130 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
db/*.db
node_modules
*.swp

BIN
client/favicon.xcf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -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 = () => {
<Routes>
<Route path="/signin" element={<SignIn />}/>
<Route path="/signup" element={<SignUp />}/>
<Route path="/user/verify-email/:token" element={<VerifyEmail />}/>
<Route path="/password" element={
<Paper>Not implemented... yet.</Paper>
}/>
<Route path="/:group" element={<Group />}/>
<Route path="/" element={
<Paper style={{
flexDirection: 'column',
display: 'flex'
}}>
<div style={{ fontWeight: 'bold' }}>Goodtimes</div>
<div>The eventual new site for the legacy Beer Tuesday... coming soon...
</div>
{ user && <div>
Logged in as {user.email}
</div> }
</Paper>
}/>
{ user && user.mailVerified &&
<Route path="/" element={<Dashboard />}/>
}
{ user && !user.mailVerified &&
<Route path="/" element={
<Paper style={{padding: "0.5rem"}}>You need to verify your email via the link sent to {user.email}.</Paper>}/>
}
{ !user &&
<Route path="/" element={<SignIn />} />
}
</Routes>
</Container>
</GlobalContext.Provider>

4
client/src/Dashboard.css Normal file
View File

@ -0,0 +1,4 @@
.Dashboard {
text-align: center;
}

78
client/src/Dashboard.js Normal file
View File

@ -0,0 +1,78 @@
import React, { useState, useEffect, useContext } from "react";
import Paper from '@mui/material/Paper';
import Moment from 'react-moment';
import {
useParams,
useNavigate
} from "react-router-dom";
import './Dashboard.css';
import { GlobalContext } from "./GlobalContext.js";
import { base } from "./Common.js";
function Dashboard() {
const { csrfToken, user, setUser } = useContext(GlobalContext);
const [ groups, setGroups ] = useState([]);
const [ error, setError ] = useState(null);
useEffect(() => {
if (!user || !csrfToken) {
return;
}
const effect = async () => {
const res = await window.fetch(
`${base}/api/v1/groups`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
}
});
const data = await res.json();
if (res.status >= 400) {
setError(data.message ? data.message : res.statusText);
return;
}
if (!data) {
return;
}
setGroups(data);
}
effect();
}, [user, setGroups, csrfToken ]);
return (
<Paper className="Dashboard">
<GlobalContext.Provider value={{user, setUser}}>
<Paper style={{
flexDirection: 'column',
display: 'flex',
width: '100%'
}}>
{ groups.map((group) => {
return <div key={group.id} style={{
flexDirection: 'column',
display: 'flex',
alignItems: 'flex-start',
padding: '0.5rem'
}}>
<div key={group.id} style={{
fontWeight: 'bold'
}}>{group.name}</div>
{ group.nextEvent &&
<div>Next event <Moment fromNow date={group.nextEvent} /> on <Moment format={'MMMM Do YYYY, h: mm: ss a'} date={group.nextEvent}/>.</div>
}
</div>;
}) }
</Paper>
</GlobalContext.Provider>
</Paper>
);
}
export default Dashboard;

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useState, useCallback } from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
@ -17,30 +17,13 @@ import {
import Gravatar from 'react-gravatar'
import { GlobalContext } from "./GlobalContext.js";
import { base } from "./Common.js";
const GoodTimesBar = () => {
const { user } = useContext(GlobalContext);
const { csrfToken, user, setUser } = useContext(GlobalContext);
const [ settings, setSettings ] = useState([]);
const navigate = useNavigate();
useEffect(() => {
if (user) {
setSettings(['Profile', 'Account', 'Dashboard', 'Logout']
.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
)));
} else {
setSettings(['Login']
.map((setting) => (
<MenuItem key={setting} onClick={handleCloseUserMenu}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
)));
}
}, [user, setSettings]);
const [anchorElNav, setAnchorElNav] = React.useState(null);
const [anchorElUser, setAnchorElUser] = React.useState(null);
@ -51,6 +34,37 @@ const GoodTimesBar = () => {
setAnchorElUser(event.currentTarget);
};
const handleUserMenu = useCallback(async (event, menu) => {
switch (menu) {
case 'Sign Out': {
const res = await window.fetch(
`${base}/api/v1/users/signout`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
}
});
const data = await res.json();
if (res.status >= 400) {
return;
}
setUser(null);
setAnchorElUser(null);
navigate('/');
break;
}
case 'Dashboard':
setAnchorElUser(null);
navigate('/');
break;
}
console.log(event);
}, [csrfToken, setUser]);
const handleCloseNavMenu = () => {
setAnchorElNav(null);
};
@ -59,6 +73,21 @@ const GoodTimesBar = () => {
setAnchorElUser(null);
};
console.log(`goodtimes-bar - user - `, user);
useEffect(() => {
if (user) {
setSettings([/*'Profile', 'Account',*/ 'Dashboard', 'Sign Out']
.map((setting) => (
<MenuItem key={setting} onClick={(e) => handleUserMenu(e, setting)}>
<Typography textAlign="center">{setting}</Typography>
</MenuItem>
)));
} else {
setSettings([]);
}
}, [user, setSettings]);
return (
<AppBar position="static">
<Container maxWidth="xl">
@ -111,14 +140,15 @@ const GoodTimesBar = () => {
>
GOODTIMES
</Typography>
<Box sx={{ flexGrow: 1, display: { xs: 'none', md: 'flex' } }}>
</Box>
<Box sx={{ flexGrow: 0 }}>
{ !user && <>
<Tooltip title="Login">
<Tooltip title="Sign In">
<Button onClick={() => navigate("/signin")} color="inherit">
Login
Sign In
</Button>
</Tooltip>
</>

3
client/src/Group.css Normal file
View File

@ -0,0 +1,3 @@
.Group {
text-align: center;
}

View File

@ -1,22 +1,55 @@
import React, { useState } from "react";
import React, { useState, useEffect, useContext } from "react";
import Paper from '@mui/material/Paper';
import {
useParams
useParams,
useNavigate
} from "react-router-dom";
import './Group.css';
import { GlobalContext } from "./GlobalContext.js";
import Paper from '@mui/material/Paper';
import { base } from "./Common.js";
function Group() {
const { group } = useParams();
const [ user, setUser ] = useState(null);
const { csrfToken, user, setUser } = useContext(GlobalContext);
const groupId = useParams().group;
const [ group, setGroup ] = useState(null);
const [ error, setError ] = useState(null);
useEffect(() => {
if (!user || !groupId || !csrfToken) {
return;
}
const effect = async () => {
const res = await window.fetch(
`${base}/api/v1/group/${groupId}/events`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
}
});
const data = await res.json();
if (res.status >= 400) {
setError(data.message ? data.message : res.statusText);
return;
}
if (!data) {
return;
}
setGroup(data);
};
effect();
}, [user, setGroup, groupId, csrfToken]);
return (
<div className="Group">
<Paper className="Group">
<GlobalContext.Provider value={{user, setUser}}>
Group: {group}
Group: {groupId}
</GlobalContext.Provider>
</div>
</Paper>
);
}

View File

@ -40,11 +40,7 @@ export default function SignIn() {
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
console.log({
email: data.get('email'),
password: data.get('password'),
});
window.fetch(`${base}/api/v1/users/login`, {
window.fetch(`${base}/api/v1/users/signin`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',

View File

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { useContext, useState } from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
@ -12,7 +12,11 @@ import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { Link as ReactLink } from "react-router-dom";
import { Link as ReactLink, useNavigate } from "react-router-dom";
import { GlobalContext } from "./GlobalContext.js";
import { base } from "./Common.js";
function Copyright(props) {
return (
@ -30,12 +34,44 @@ function Copyright(props) {
const theme = createTheme();
export default function SignUp() {
const { setUser, csrfToken } = useContext(GlobalContext);
const [error, setError] = useState(undefined);
const navigate = useNavigate();
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
console.log({
email: data.get('email'),
password: data.get('password'),
window.fetch(`${base}/api/v1/users/signup`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
},
body: JSON.stringify({
email: data.get('email'),
firstName: data.get('firstName'),
familyName: data.get('lastName'),
password: data.get('password')
})
})
.then(async (res) => {
return [res.status, res.statusText, await res.json()];
})
.then(([status, statusText, data]) => {
if (status >= 400) {
setError(data.message ? data.message : statusText);
return null;
} if (!data) {
return;
}
setUser(data);
navigate("/");
})
.catch((error) => {
console.error(`Error: `, error);
setError(`Error authenticating.`);
});
};
@ -102,6 +138,10 @@ export default function SignUp() {
/>
</Grid>
</Grid>
{error && <Typography component="h2" sx={{ color: 'red' }} >
{error}
</Typography>
}
<Button
type="submit"
fullWidth

73
client/src/VerifyEmail.js Normal file
View File

@ -0,0 +1,73 @@
import React, { useState, useEffect, useContext } from "react";
import {
useParams,
useNavigate
} from "react-router-dom";
import Paper from '@mui/material/Paper';
import Typography from '@mui/material/Typography';
import './Group.css';
import { GlobalContext } from "./GlobalContext.js";
import { base } from "./Common.js";
function Group() {
const navigate = useNavigate();
const { csrfToken } = useContext(GlobalContext);
const { token } = useParams();
const [ user, setUser ] = useState(null);
const [error, setError ] = useState(undefined);
useEffect(() => {
if (!token || !csrfToken) {
return;
}
const effect = async () => {
const res = await window.fetch(`${base}/api/v1/users/verify-email`, {
method: 'POST',
cache: 'no-cache',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'CSRF-Token': csrfToken
},
body: JSON.stringify({
token
})
});
const data = await res.json();
if (res.status >= 400) {
setError(data.message ? data.message : res.statusText);
return;
}
if (!data) {
return;
}
setUser(data);
navigate("/");
}
effect();
}, [setError, token, csrfToken, navigate]);
return (
<Paper className="Group" style={{display: "flex", flexDirection: "column"}}>
<GlobalContext.Provider value={{user, setUser}}>
<div>
Verifying authentication token.
</div>
</GlobalContext.Provider>
{error &&
<Paper style={{
padding: "0.5rem"
}}>
<Typography component="h2" sx={{ color: 'red' }} >
{error}
</Typography>
</Paper>
}
</Paper>
);
}
export default Group;

Binary file not shown.

Binary file not shown.

View File

@ -28,7 +28,7 @@ app.use(csrf({
cookie: true
}));
const ws = require('express-ws')(app, server);
//const ws = require('express-ws')(app, server);
require("./console-line.js"); /* Monkey-patch console.log with line numbers */
@ -101,7 +101,7 @@ require("./db/groups").then(function(db) {
}).then(function() {
console.log("DB connected. Opening server.");
server.listen(serverConfig.port, () => {
console.log(`http/ws server listening on ${serverConfig.port}`);
console.log(`http server listening on ${serverConfig.port}`);
});
}).catch(function(error) {
console.error(error);

View File

@ -1,8 +1,6 @@
"use strict";
const fs = require('fs'),
path = require('path'),
Sequelize = require('sequelize'),
const Sequelize = require('sequelize'),
config = require('config');
function init() {
@ -19,14 +17,31 @@ function init() {
autoIncrement: true
},
name: Sequelize.STRING,
ownerId: Sequelize.INTEGER
}, {
timestamps: false,
classMethods: {
associate: function() {
}
associate: function() {}
}
});
const GroupUsers = db.sequelize.define('groupuser', {
groupId: {
type: Sequelize.INTEGER,
allowNull: false,
references: {
model: Group,
key: 'id'
}
},
userId: {
type: Sequelize.INTEGER,
allowNull: false,
}
}, {
timestamps: false
});
return db.sequelize.sync({
force: false
}).then(function () {

View File

@ -16,15 +16,15 @@ function init() {
primaryKey: true,
autoIncrement: true
},
displayName: Sequelize.STRING,
notes: Sequelize.STRING,
familyName: Sequelize.STRING,
firstName: Sequelize.STRING,
uid: Sequelize.STRING,
md5: Sequelize.STRING,
md5: Sequelize.STRING, /* md5 of email address for Gravatar */
authToken: Sequelize.STRING,
authDate: Sequelize.DATE,
authenticated: Sequelize.BOOLEAN,
mailVerified: Sequelize.BOOLEAN,
mail: Sequelize.STRING,
email: Sequelize.STRING,
memberSince: Sequelize.DATE,
password: Sequelize.STRING, /* SHA hash of user supplied password */
passwordExpires: Sequelize.DATE

View File

@ -4,25 +4,33 @@ const config = require("config"),
crypto = require("crypto"),
hb = require("handlebars");
const createTransport = require('nodemailer').createTransport;
const transporter = createTransport({
host: 'ketrenos.com',
pool: true,
port: 25
});
const templates = {
"verify": {
"html": [
"<p>Hello {{username}},</p>",
"<p>Hello {{firstName}},</p>",
"",
"<p>Welcome to <b>goodtimes.ketrenos.com</b>. You are almost done creating your account. ",
"Before you can access the system, you must verify your email address.</p>",
"Before you can access the system, you must verify your email address {{email}}.</p>",
"",
"<p>To do so, simply access this link:</p>",
"<p><a href=\"{{url}}{{secret}}\">VERIFY {{mail}} ADDRESS</a></p>",
"<p><a href=\"{{url}}{{secret}}\">VERIFY ADDRESS</a></p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].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": [
"<p>Hello {{username}},</p>",
"",
"<p>You changed your password on <b>ketrenos.com</b>.</p>",
"<p>The user {{email}} verified their address and is now active",
"on Goodtimes.</p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].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": [
"<p>Hello {{firstName}},</p>",
"",
"<p>You changed your password on <b>goodtimes.ketrenos.com</b>.</p>",
"",
"<p>Sincerely,</p>",
"<p>James</p>"
].join("\n"),
"text": [
"Hello {{firstName}},",
"",
"You changed your password on goodtimes.ketrenos.com.",
"",
"Sincerely,</p>",
"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
}

View File

@ -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;

View File

@ -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;
}