diff --git a/client/src/App.js b/client/src/App.js index c4e7ad2..24583d8 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -25,6 +25,8 @@ const App = () => { const [ user, setUser ] = useState(null); const [ csrfToken, setCsrfToken ] = useState(undefined); const [ loading, setLoading ] = useState(true); + const [ googleApiKey, setGoogleApiKey ] = useState(undefined); + const [ error, setError] = useState(undefined); useEffect(() => { const effect = async () => { @@ -70,11 +72,44 @@ const App = () => { effect(); }, [csrfToken, setUser]); + + useEffect(() => { + if (!csrfToken || googleApiKey) { + return; + } + const effect = async () => { + const res = await window.fetch( + `${base}/api/v1/locations/google-api-key`, { + method: 'GET', + 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; + } + setGoogleApiKey(data.key); + } + effect(); + }, [csrfToken, googleApiKey, setGoogleApiKey]); + return (
- + + { error && {error} } }/> }/> diff --git a/client/src/Dashboard.js b/client/src/Dashboard.js index c32c34e..57a3608 100644 --- a/client/src/Dashboard.js +++ b/client/src/Dashboard.js @@ -47,36 +47,81 @@ function Dashboard() { effect(); }, [user, setGroups, csrfToken ]); + const upcomingEvents = groups + .filter(group => group.nextEvent > Date.now()); + return ( - +
- + {upcomingEvents && upcomingEvents.length && + +
+ Upcoming events +
- { groups.map((group) => { - return ; - }) } -
+ { upcomingEvents + .map((group) => { + return ; + }) + } +
+ } + { groups && groups.length && + +
+ Groups +
+ { groups + .map((group) => { + return ; + }) + } +
+ }
- +
); } diff --git a/client/src/Group.js b/client/src/Group.js index 6184c9e..5a9e97e 100644 --- a/client/src/Group.js +++ b/client/src/Group.js @@ -11,11 +11,10 @@ import { base } from "./Common.js"; import { Location } from "./Location.js"; function Group() { - const { csrfToken, user } = useContext(GlobalContext); + const { csrfToken, user, setError } = useContext(GlobalContext); const groupId = useParams().group; const [ group, setGroup ] = useState(undefined); const [ events, setEvents ] = useState(null); - const [ error, setError ] = useState(null); const [ locations, setLocations ] = useState([]); useEffect(() => { @@ -45,7 +44,7 @@ function Group() { setLocations(data); }; effect(); - }, [user, setGroup, groupId, csrfToken]); + }, [user, setGroup, groupId, csrfToken, setError]); useEffect(() => { if (!user || !groupId || !csrfToken) { @@ -74,7 +73,7 @@ function Group() { setEvents(data); }; effect(); - }, [user, setEvents, groupId, csrfToken]); + }, [user, setEvents, groupId, csrfToken, setError]); useEffect(() => { if (!user || !groupId || !csrfToken) { @@ -103,7 +102,7 @@ function Group() { setGroup(data[0]); }; effect(); - }, [user, setGroup, groupId, csrfToken]); + }, [user, setGroup, groupId, csrfToken, setError]); return ( - { error &&
{error}
} - { !error && group && <> + { group && <>
{group.name}
Locations
{ locations.map(location => diff --git a/client/src/Location.js b/client/src/Location.js index 3664307..e62b630 100644 --- a/client/src/Location.js +++ b/client/src/Location.js @@ -11,42 +11,12 @@ import { base } from "./Common.js"; function Location(props) { const propLocation = props.location; - const { csrfToken } = useContext(GlobalContext); - const [googleApiKey, setGoogleApiKey] = useState(undefined); + const { csrfToken, googleApiKey, setError } = useContext(GlobalContext); const [ location, setLocation ] = useState( typeof propLocation === 'object' ? propLocation : undefined); - const [ error, setError ] = useState(null); const locationId = typeof propLocation === 'number' ? propLocation : undefined; - useEffect(() => { - if (!csrfToken || googleApiKey) { - return; - } - const effect = async () => { - const res = await window.fetch( - `${base}/api/v1/locations/google-api-key`, { - method: 'GET', - 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; - } - setGoogleApiKey(data.key); - } - effect(); - }, [csrfToken, googleApiKey, setGoogleApiKey]); - useEffect(() => { if (!csrfToken || location || !locationId) { return; @@ -139,10 +109,7 @@ function Location(props) { marginTop: '0.25rem', marginBottom: '0.25rem' }}> - { error &&
{error}
} - { !error && <> - { createLocation(location) } - } + { createLocation(location) }
); } diff --git a/server/event-data.js b/server/event-data.js new file mode 100644 index 0000000..7cb547f --- /dev/null +++ b/server/event-data.js @@ -0,0 +1,8 @@ +const originalEvents = [ { + id: 1, + name: 'Tuesday', + event: 'tuesday', + date: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */ +} ]; +originalEvents.forEach((item, index) => item.id = index + 1); +module.exports = originalEvents; \ No newline at end of file diff --git a/server/group-data.js b/server/group-data.js new file mode 100644 index 0000000..c8f6ba7 --- /dev/null +++ b/server/group-data.js @@ -0,0 +1,10 @@ +const originalGroups = [ { + ownerId: 1, + name: 'Beer Tuesday', + group: 'beer-tuesday', + nextEvent: Date.now() + 86400 * 14 * 1000, /* 2 weeks from now */ + nextEventId: 1 +} ]; +originalGroups.forEach((item, index) => item.id = index + 1); + +module.exports = originalGroups; \ No newline at end of file diff --git a/server/location-data.js b/server/location-data.js index ad4cedb..b97333c 100644 --- a/server/location-data.js +++ b/server/location-data.js @@ -1,4 +1,4 @@ -module.exports = [{'id':1,'name':'CPR','location':'Cornelius Pass & Imbrie Dr ','map':'http://goo.gl/maps/ANsh2','url':'','note':'Cornelius Pass Roadhouse; standard McMenamin food and drinks. The Rubinator (Ruby + Terminator) is a common favorite, as are the rotating nitro offerings.\n\nHappy hour foods aren\'t that thrilling; tots, fries, etc.','disabled':0,'beerlist':''}, +const originalLocations = [{'id':1,'name':'CPR','location':'Cornelius Pass & Imbrie Dr ','map':'http://goo.gl/maps/ANsh2','url':'','note':'Cornelius Pass Roadhouse; standard McMenamin food and drinks. The Rubinator (Ruby + Terminator) is a common favorite, as are the rotating nitro offerings.\n\nHappy hour foods aren\'t that thrilling; tots, fries, etc.','disabled':0,'beerlist':''}, {'id':2,'name':'Orenco Taphouse','location':'Couple blocks off Cornell in Orenco Station area.','map':'http://goo.gl/maps/qoOxl','url':'http://orencotaphouse.com','note':'A wide variety of frequently changing taps; a good place to have a beer or two. \n\nNo food or appetizers, or happy hour pricing. If its crowded, or the TVs are all on, it can be a little loud...','disabled':0,'beerlist':'http://orencotaphouse.com/?page_id=5'}, {'id':3,'name':'Dugout','location':'Just down the street from Jones Farm, across Cornell in the strip mall near the Dollar Store','map':'https://goo.gl/HXc5oq','url':'https://www.facebook.com/TheDugoutGourmetDeliBeerBarInc','note':'','disabled':0,'beerlist':''}, {'id':4,'name':'Raccoon Lodge','location':'Beaverton Hillsdale Hwy a couple miles east of 217.','map':'http://goo.gl/maps/fbFAa','url':'http://www.raclodge.com','note':'OMG! The fruit beers! This is the west-side spot to drink Cascade Brewing\'s various beers. In addition to the sours, they have a wide variety of other beer types.\n\nHappy hour prices aren\'t that great, and only apply to the "standard" Cascade Brewing beers--not the sours :(','disabled':0,'beerlist':'http://www.raclodge.com/PDFs/cascade_brewing_ales_new.do.do.do.doc_oct_20.3.1.docuse.doc.docx'}, @@ -30,3 +30,7 @@ module.exports = [{'id':1,'name':'CPR','location':'Cornelius Pass & Imbrie Dr ', {'id':31,'name':'Iron Tap Station','location':'Across Hall Blvd from Washington Square, in a strip mall just a few doors down from Kitchen Kaboodle and Lamps Plus.','map':'https://goo.gl/SDWgWB','url':'http://www.irontapstation.com/','note':'','disabled':0,'beerlist':'http://www.irontapstation.com/#menu-section'}, {'id':32,'name':'Craft Pour House','location':'16055 SW Regatta Ln #700\nBeaverton, OR 97006\n\nNext door to where Monteaux\'s used to be.','map':'https://goo.gl/maps/VJpMpFS5RPC2','url':'http://craftpourhouse.com/','note':'They\'re new!','disabled':0,'beerlist':'http://craftpourhouse.com/'}, {'id':33,'name':'Vertigo Brewing','location':'Located in the back of an industrial complex, this brewer is not to be missed!\n\n21420 NW Nicholas Ct.\nSuite D-6 & D-7\nHillsboro, OR 97124','map':'https://goo.gl/ABTbnD','url':'http://www.vertigobrew.com/','note':'','disabled':0,'beerlist':'http://www.vertigobrew.com/the-beer/'}]; + +originalLocations.forEach((item, index) => item.id = index + 1); + +module.exports = originalLocations; \ No newline at end of file diff --git a/server/routes/events.js b/server/routes/events.js index e5e47a0..6248e37 100644 --- a/server/routes/events.js +++ b/server/routes/events.js @@ -1,337 +1,64 @@ 'use strict'; -const express = require('express'), - { sendVerifyMail, sendPasswordChangedMail, sendVerifiedMail } = - require('../lib/mail'), - crypto = require('crypto'); - +const config = require('config'); +const express = require('express'); const router = express.Router(); -const autoAuthenticate = 1; -router.get('/', (req, res/*, next*/) => { - console.log('GET /users/'); +const originalEvents = require('../event-data.js'); - return getSessionUser(req).then((user) => { - if (typeof user === 'string') { - return res.status(403).send({ message: user }); - } - return res.status(200).send(user); - }); +router.put('/', (req, res) => { + const event = req.body; + event.id = originalEvents.length; + originalEvents.push(event); + res.status(200).send([event]); }); -router.put('/password', async (req, res) => { - console.log('/users/password'); - const db = req.app.locals.db; - - const changes = { - currentPassword: req.query.c || req.body.c, - newPassword: req.query.n || req.body.n - }; - - if (!changes.currentPassword || !changes.newPassword) { - return res.status(400).send( - 'Missing current password and/or new password.'); - } - - if (changes.currentPassword == changes.newPassword) { - return res.status(400).send( - 'Attempt to set new password to current password.'); - } - - const user = await getSessionUser(req); - if (typeof user === 'string') { - return res.status(403).send({ message: user }); - } - return db.sequelize.query('SELECT id FROM users ' + - 'WHERE uid=:username AND password=:password', { - replacements: { - username: user.username, - password: crypto.createHash('sha256').update(changes.currentPassword).digest('base64') - }, - type: db.Sequelize.QueryTypes.SELECT, - raw: true - }).then(function(users) { - if (users.length != 1) { - return null; - } - return user; - }).then(function(user) { - if (!user) { - console.log('Invalid password'); - /* Invalid password */ - res.status(401).send('Invalid password'); - return null; - } - - return db.sequelize.query('UPDATE users SET password=:password WHERE uid=:username', { - replacements: { - username: user.username, - password: crypto.createHash('sha256').update(changes.newPassword).digest('base64') - } - }).then(function() { - console.log('Password changed for user ' + user.username + ' to \'' + changes.newPassword + '\'.'); - - res.status(200).send(user); - user.id = req.session.userId; - return sendPasswordChangedMail(db, req, user); - }); - }); +router.get('/google-api-key', (req, res) => { + return res.status(200).send({ key: config.get('googleApi') }); }); -router.get('/csrf', (req, res) => { - const token = req.csrfToken(); - console.log('/users/csrf', token); - res.json({ csrfToken: token }); +router.get('/:eventId?', (req, res) => { + const { eventId } = req.params; + if (eventId) { + const event = originalEvents.find( + item => item.id === eventId + ); + if (!event) { + return res.status(404).send({ message: `Event ${eventId} not found.` }); + } + return res.status(200).send([event]); + } + + return res.status(200).send(originalEvents); }); -router.post('/signup', function(req, res) { - console.log('/users/signup'); - const db = req.app.locals.db; - - const user = { - uid: req.body.email, - familyName: req.body.familyName, - firstName: req.body.firstName, - password: req.body.password, - email: req.body.email, - }; - - if (!user.uid - || !user.email - || !user.password - || !user.familyName - || !user.firstName) { - return res.status(400).send({ - message: 'Missing email address, password, and/or name.' - }); +router.post('/:eventId', (req, res) => { + const { eventId } = req.params; + if (!eventId) { + return res.status(400).send({ message: `Invalid event.` }); } - - user.password = crypto.createHash('sha256') - .update(user.password).digest('base64'); - user.md5 = crypto.createHash('md5') - .update(user.email).digest('base64'); - - return db.sequelize.query('SELECT * FROM users WHERE uid=:uid', { - replacements: user, - type: db.Sequelize.QueryTypes.SELECT, - raw: true - }).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.email)) { - const error = `Invalid email address: ${user.email}.`; - console.log(error); - return res.status(401).send({ - message: error - }); - } - - try { - if (results.length != 0) { - await db.sequelize.query('UPDATE users ' + - 'SET mailVerified=0'); - req.session.userId = results[0].id; - } else { - let [, metadata] = await db.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) { - if (typeof user === 'string') { - return res.status(403).send({ message: user }); - } - res.status(200).send(user); - user.id = req.session.userId; - return sendVerifyMail(db, req, user); - }); - } catch (error) { - console.error(error); - } - }); -}); - -const getSessionUser = async (req) => { - const db = req.app.locals.db; - - console.log(req.session); - if (!req.session || !req.session.userId) { - return 'Unauthorized. You must be logged in.'; - } - - let query = 'SELECT ' + - 'uid AS username,firstName,familyName,mailVerified,authenticated,memberSince,email,md5 ' + - 'FROM users WHERE id=:id'; - const results = await db.sequelize.query(query, { - replacements: { - id: req.session.userId - }, - type: db.Sequelize.QueryTypes.SELECT, - raw: true - }); - - if (results.length != 1) { - return 'Invalid account.'; - } - - const user = results[0]; - - if (!user.mailVerified) { - user.restriction = user.restriction || 'Email address not verified.'; - } else if (!user.authenticated) { - user.restriction = user.restriction || 'Accout not authorized.'; - } - - /* Strip out any fields that shouldn't be there. - * The allowed fields are: */ - const allowed = [ - 'maintainer', 'username', 'firstName', 'familyName', 'mailVerified', 'authenticated', 'name', 'email', 'restriction', 'md5' - ]; - for (let field in user) { - if (allowed.indexOf(field) == -1) { - delete user[field]; - } - } - return user; -}; - -router.post('/verify-email', async (req, res) => { - console.log('/users/verify-email'); - const key = req.body.token; - const db = req.app.locals.db; - - let results = await db.sequelize.query( - 'SELECT * FROM authentications WHERE key=:key', { - replacements: { key }, - type: db.sequelize.QueryTypes.SELECT - } + const event = originalEvents.find( + item => item.id === eventId ); - - 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 db.sequelize.query( - 'UPDATE users SET mailVerified=1 WHERE id=:userId', { - replacements: token - } - ).then(function () { - return db.sequelize.query( - 'DELETE FROM authentications WHERE key=:key', { - replacements: { key } - } - ); - }).then(function () { - return db.sequelize.query( - 'SELECT * FROM users WHERE id=:userId', { - replacements: token, - type: db.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(db, req, user); - req.session.userId = user.id; - }).then(() => { - return getSessionUser(req).then((user) => { - if (typeof user === 'string') { - return res.status(403).send({ message: user }); - } - return res.status(200).send(user); - }); - }); + if (!event) { + res.status(404).send({ message: `Event ${eventId} not found.` }); } + res.status(200).send([event]); }); -router.post('/signin', (req, res) => { - console.log('/users/signin'); - const db = req.app.locals.db; - - let { email, password } = req.body; - - if (!email || !password) { - return res.status(400).send({ - message: 'Missing email and/or password' - }); +router.delete('/:eventId', (req, res) => { + const { eventId } = req.params; + if (!eventId) { + return res.status(400).send({ message: `Invalid event.` }); } - - console.log('Looking up user in DB.'); - - let query = 'SELECT ' + - 'id,mailVerified,authenticated,' + - 'uid AS username,' + - 'familyName,firstName,email ' + - 'FROM users WHERE uid=:username AND password=:password'; - return db.sequelize.query(query, { - replacements: { - username: email, - password: crypto.createHash('sha256').update(password).digest('base64') - }, - type: db.Sequelize.QueryTypes.SELECT - }).then(function(users) { - if (users.length != 1) { - return null; - } - let user = users[0]; - req.session.userId = user.id; - return user; - }).then(function(user) { - if (!user) { - console.log(email + ' not found (or invalid password.)'); - req.session.userId = null; - return res.status(401).send({ - message: 'Invalid sign in credentials' - }); - } - - let message = 'Logged in as ' + user.email + ' (' + user.id + ')'; - if (!user.mailVerified) { - console.log(message + ', who is not verified email.'); - } else if (!user.authenticated) { - console.log(message + ', who is not authenticated.'); - } else { - console.log(message); - } - - return getSessionUser(req).then((user) => { - if (typeof user === 'string') { - return res.status(403).send({ message: user }); - } - return res.status(200).send(user); - }); - }); -}); - -router.post('/signout', (req, res) => { - console.log('/users/signout'); - if (req.session && req.session.userId) { - req.session.userId = null; + const eventIndex = originalEvents.findIndex( + item => item.id === eventId + ); + if (eventIndex === -1) { + res.status(404).send({ message: `Event ${eventId} not found.` }); } - res.status(200).send({}); + originalEvents.splice(eventIndex, 1); + res.status(200).send({ message: `Event ${eventId} deleted.` }); }); module.exports = router; diff --git a/server/routes/groups.js b/server/routes/groups.js index 684f0cd..dff9f92 100755 --- a/server/routes/groups.js +++ b/server/routes/groups.js @@ -1,13 +1,8 @@ const express = require('express'), router = express.Router(); -const originalGroups = [ { - id: 1, - ownerId: 1, - name: 'Beer Tuesday', - group: 'beer-tuesday', - nextEvent: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */ -} ]; +const originalGroups = require('../group-data.js'); +const originalEvents = require('../event-data.js'); router.get('/:group?', async (req, res/*, next*/) => { const { group } = req.params; @@ -30,13 +25,7 @@ router.get('/:group?', async (req, res/*, next*/) => { }); router.get('/:group/events', async (req, res/*, next*/) => { - return res.status(200).send( - [{ - id: 1, - name: 'Tuesday', - date: Date.now() + 86400 * 14 * 1000 /* 2 weeks from now */ - }] - ); + return res.status(200).send(originalEvents); }); router.post('/:group', async (req, res) => { diff --git a/server/routes/locations.js b/server/routes/locations.js index ec1841b..f2c438b 100644 --- a/server/routes/locations.js +++ b/server/routes/locations.js @@ -5,7 +5,6 @@ const express = require('express'); const router = express.Router(); const originalLocations = require('../location-data.js'); -originalLocations.forEach((item, index) => item.id = index + 1); router.put('/', (req, res) => { const location = req.body;