Authentication working
Signed-off-by: James Ketrenos <james_eikona@ketrenos.com>
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
||||
db/*.db
|
||||
node_modules
|
||||
*.swp
|
||||
|
BIN
client/favicon.xcf
Normal file
BIN
client/public/assets/favicon-128.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
client/public/assets/favicon-152.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
client/public/assets/favicon-167.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
client/public/assets/favicon-180.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
client/public/assets/favicon-192.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
client/public/assets/favicon-256.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
client/public/assets/favicon-32.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -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 />}/>
|
||||
{ user && user.mailVerified &&
|
||||
<Route path="/" element={<Dashboard />}/>
|
||||
}
|
||||
{ user && !user.mailVerified &&
|
||||
<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>
|
||||
}/>
|
||||
<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
@ -0,0 +1,4 @@
|
||||
.Dashboard {
|
||||
text-align: center;
|
||||
}
|
||||
|
78
client/src/Dashboard.js
Normal 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;
|
@ -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
@ -0,0 +1,3 @@
|
||||
.Group {
|
||||
text-align: center;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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({
|
||||
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'),
|
||||
password: data.get('password'),
|
||||
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
@ -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;
|
BIN
db/groups.db
BIN
db/users.db
@ -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);
|
||||
|
@ -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,12 +17,29 @@ 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({
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)", {
|
||||
|
||||
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
|
||||
}).spread(function(results, metadata) {
|
||||
});
|
||||
req.session.userId = metadata.lastID;
|
||||
}).then(function() {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|