Logins and logouts (sessions) are working

This commit is contained in:
2019-05-08 11:35:57 +10:00
parent 0d4bb2f57f
commit fbb37ad2d9
15 changed files with 477 additions and 74 deletions
+113 -45
View File
@@ -1424,29 +1424,48 @@
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.0.0",
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
}
}
},
@@ -1669,9 +1688,9 @@
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cacache": {
"version": "11.3.2",
@@ -2563,10 +2582,51 @@
"vary": "~1.1.2"
},
"dependencies": {
"body-parser": {
"version": "1.18.3",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
"integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
"requires": {
"bytes": "3.0.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "~1.6.3",
"iconv-lite": "0.4.23",
"on-finished": "~2.3.0",
"qs": "6.5.2",
"raw-body": "2.3.3",
"type-is": "~1.6.16"
}
},
"bytes": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"unpipe": "1.0.0"
}
}
}
},
@@ -3079,13 +3139,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -3098,18 +3156,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -3212,8 +3267,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@@ -3223,7 +3277,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -3236,7 +3289,6 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -3336,8 +3388,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -3347,7 +3398,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -3453,7 +3503,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -5318,23 +5367,37 @@
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
},
"raw-body": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
"integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.0.0",
"http-errors": "1.6.3",
"iconv-lite": "0.4.23",
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"dependencies": {
"iconv-lite": {
"version": "0.4.23",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz",
"integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==",
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
}
}
},
@@ -6311,6 +6374,11 @@
"repeat-string": "^1.6.1"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"trim-right": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
+1
View File
@@ -19,6 +19,7 @@
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.5",
"bcrypt": "^3.0.6",
"body-parser": "^1.19.0",
"dotenv": "^8.0.0",
"express": "^4.16.4",
"forever": "^1.0.0",
+72 -1
View File
@@ -127,7 +127,78 @@ function verify(connection) {
}
}
function login(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) {
res.write('<p>Invalid login data</p>');
res.end();
return;
}
//find this email's information
let query = 'SELECT id, username, salt, hash FROM accounts WHERE email = ?;';
connection.query(query, [fields.email], (err, results) => {
if (err) throw err;
//found this email?
if (results.length === 0) {
res.status(400).write('Incorrect email or password');
res.end();
return;
}
//gen a new hash from the salt and password
bcrypt.hash(fields.password, results[0].salt, (err, newHash) => {
if (err) throw err;
//compare the passwords
if (results[0].hash !== newHash) {
res.status(400).write('Incorrect email or password');
res.end();
return;
}
//create the new session
let rand = Math.floor(Math.random() * 100000);
let query = 'INSERT INTO sessions (accountId, token) VALUES (?, ?);';
connection.query(query, [results[0].id, rand], (err) => {
if (err) throw err;
//send json containing the account info
res.status(200).json({
id: results[0].id,
email: fields.email,
username: results[0].username,
token: rand
});
});
});
});
});
}
}
function logout(connection) {
return (req, res) => {
let query = 'DELETE FROM sessions WHERE sessions.accountId IN (SELECT accounts.id FROM accounts WHERE email = ?) AND token = ?;';
connection.query(query, [req.body.email, req.body.token], (err) => {
if (err) throw err;
});
}
}
module.exports = {
signup: signup,
verify: verify
verify: verify,
login: login,
logout: logout
};
+5
View File
@@ -5,8 +5,11 @@ require('dotenv').config();
let express = require('express');
let app = express();
let http = require('http').Server(app);
let bodyParser = require('body-parser');
let path = require('path');
app.use(bodyParser.json());
//database
let { connectToDatabase } = require('./database.js');
let connection = connectToDatabase(); //uses .env
@@ -15,6 +18,8 @@ let connection = connectToDatabase(); //uses .env
let accounts = require('./accounts.js');
app.post('/signup', accounts.signup(connection));
app.get('/verify', accounts.verify(connection));
app.post('/login', accounts.login(connection));
app.post('/logout', accounts.logout(connection));
//static directories
app.use('/styles', express.static(path.resolve(__dirname + '/../public/styles')) );
+12 -2
View File
@@ -1,4 +1,4 @@
CREATE TABLE signups (
CREATE TABLE IF NOT EXISTS signups (
email VARCHAR(320) UNIQUE,
username VARCHAR(100) UNIQUE,
salt VARCHAR(50),
@@ -7,7 +7,7 @@ CREATE TABLE signups (
verify INTEGER DEFAULT 0
);
CREATE TABLE accounts (
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE,
td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
@@ -17,3 +17,13 @@ CREATE TABLE accounts (
hash VARCHAR(100)
);
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER UNSIGNED AUTO_INCREMENT PRIMARY KEY UNIQUE,
td TIMESTAMP DEFAULT CURRENT_TIMESTAMP(),
accountId INTEGER UNSIGNED,
token INTEGER DEFAULT 0,
CONSTRAINT FOREIGN KEY fk_accountId(accountId) REFERENCES accounts(id) ON UPDATE CASCADE ON DELETE CASCADE
);
+2 -1
View File
@@ -1,3 +1,4 @@
DROP TABLE signups;
DROP TABLE accounts;
DROP TABLE profiles;
DROP TABLE sessions;
#DROP TABLE profiles;
+18
View File
@@ -0,0 +1,18 @@
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export function login(id, email, username, token) {
return {
type: LOGIN,
id: id,
email: email,
username: username,
token: token
};
}
export function logout() {
return {
type: LOGOUT
};
}
-7
View File
@@ -1,7 +0,0 @@
export const ACTION_NAME = 'ACTION_NAME';
export function actionName() {
return {
type: ACTION_NAME
};
}
+27 -2
View File
@@ -5,6 +5,8 @@ import PropTypes from 'prop-types';
//panels
import Signup from '../panels/signup.jsx';
import Login from '../panels/login.jsx';
import Logout from '../panels/logout.jsx';
class Home extends React.Component {
constructor(props) {
@@ -13,10 +15,33 @@ class Home extends React.Component {
}
render() {
//well this is goofy
let SidePanel;
if (this.props.id) {
SidePanel = () => {
return (
<div>
<p>You are logged in.</p>
<Logout />
</div>
);
};
} else {
SidePanel = () => {
return (
<div>
<Signup />
<Login />
</div>
);
};
}
return (
<div className='page'>
<p>This is the home page.</p>
<Signup />
<SidePanel />
</div>
);
}
@@ -24,7 +49,7 @@ class Home extends React.Component {
function mapStoreToProps(store) {
return {
//
id: store.account.id
}
}
+131
View File
@@ -0,0 +1,131 @@
import React from 'react';
import { connect } from 'react-redux';
import { login } from '../../actions/accounts.js';
import { validateEmail } from '../../../common/utilities.js';
class Login extends React.Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
warning: ''
};
}
render() {
let warningStyle = {
display: this.state.warning.length > 0 ? 'flex' : 'none'
};
return (
<div className='panel'>
<h1>Login</h1>
<div className='warning' style={warningStyle}>
<p>{this.state.warning}</p>
</div>
<form action='/login' method='post' onSubmit={(e) => this.submit(e)}>
<div>
<label>Email:</label>
<input type='text' name='email' value={this.state.email} onChange={this.updateEmail.bind(this)} />
</div>
<div>
<label>Password:</label>
<input type='password' name='password' value={this.state.password} onChange={this.updatePassword.bind(this)} />
</div>
<button type='submit'>Login</button>
</form>
</div>
);
}
submit(e) {
e.preventDefault();
if (!this.validateInput()) {
return;
}
//build the XHR
let form = e.target;
let formData = new FormData(form);
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
let json = JSON.parse(xhr.responseText);
this.props.login(json.id, json.email, json.username, json.token);
}
else if (xhr.status === 400) {
this.setWarning(xhr.responseText);
}
}
};
//send the XHR
xhr.open('POST', form.action, true);
xhr.send(formData);
}
validateInput(e) {
if (!validateEmail(this.state.email)) {
this.setWarning('Invalid Email');
return false;
}
if (this.state.password.length < 8) {
this.setWarning('Minimum password length is 8 characters');
return false;
}
return true;
}
setWarning(s) {
this.setState({
warning: s
});
}
clearInput() {
this.setState({
email: '',
password: '',
warning: ''
});
}
updateEmail(evt) {
this.setState({
email: evt.target.value
});
}
updatePassword(evt) {
this.setState({
password: evt.target.value
});
}
}
function mapStoreToProps(store) {
return {
//
}
}
function mapDispatchToProps(dispatch) {
return {
login: (id, email, username, token) => { dispatch(login(id, email, username, token)) }
}
}
Login = connect(mapStoreToProps, mapDispatchToProps)(Login);
export default Login;
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import { connect } from 'react-redux';
import { logout } from '../../actions/accounts.js';
class Logout extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<button type='submit' onClick={(e) => this.submit(e)}>Logout</button>
);
}
submit(e) {
e.preventDefault();
//build the XHR
let xhr = new XMLHttpRequest();
xhr.open('POST', '/logout', true);
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
xhr.send(JSON.stringify({
email: this.props.email,
token: this.props.token
}));
this.props.logout();
}
}
function mapStoreToProps(store) {
return {
email: store.account.email,
token: store.account.token
}
}
function mapDispatchToProps(dispatch) {
return {
logout: () => { dispatch(logout()) }
}
}
Logout = connect(mapStoreToProps, mapDispatchToProps)(Logout);
export default Logout;
+12 -2
View File
@@ -7,17 +7,27 @@ import thunk from 'redux-thunk';
import DevTools from './dev_tools.jsx';
import App from './components/app.jsx';
import reducer from './reducers/reducer.jsx';
import reducer from './reducers/reducer.js';
//persistence
let ITEM_NAME = 'account.kingdombattles';
let account = localStorage.getItem(ITEM_NAME);
account = account ? JSON.parse(account) : {};
var store = createStore(
reducer,
{}, //initial state
{ account: account }, //initial state
compose(
applyMiddleware(thunk),
DevTools.instrument()
)
);
//persistence
store.subscribe(() => {
localStorage.setItem(ITEM_NAME, JSON.stringify(store.getState().account));
});
//start the process
ReactDOM.render(
<Provider store={store}>
+29
View File
@@ -0,0 +1,29 @@
import { LOGIN, LOGOUT } from "../actions/accounts.js";
const initialStore = {
id: 0,
email: '',
username: '',
token: 0
};
export function accountReducer(store = initialStore, action) {
switch(action.type) {
case LOGIN:
let newStore = JSON.parse(JSON.stringify(initialStore));
newStore.id = action.id;
newStore.email = action.email;
newStore.username = action.username;
newStore.token = action.token;
return newStore;
case LOGOUT:
return initialStore;
default:
return store;
}
}
+8
View File
@@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import { accountReducer } from './accounts.js';
//compile all reducers together
export default combineReducers({
account: accountReducer
});
-14
View File
@@ -1,14 +0,0 @@
import { ACTION_NAME } from "../actions/actions.jsx";
const initialState = {};
export default function reducer(state = initialState, action) {
switch(action.type) {
case ACTION_NAME:
//DO NOTHING
return state;
default:
return state;
}
}