var express = require("express"); var hbs = require('express-hbs'); require('handlebars-form-helpers').register(hbs.handlebars); var nodemailer = require('nodemailer'); var courtsopenUtils = require('./courtsopenUtils.js'); var bodyParser = require("body-parser"); var app = express(); var winston = require('winston'); winston.add(winston.transports.File, { filename: './logs/courtsopen.log', maxsize: 5000000 }); // 5MB var fileSystem = require('fs'); var passport = require('passport'); var Strategy = require('passport-local').Strategy; var db = require('./db-auth'); // Make this db-auth.../db already taken by PostgreSQL linnked container!!! // Configure the local strategy for use by Passport. // The local strategy require a `verify` function which receives the credentials // (`username` and `password`) submitted by the user. The function must verify // that the password is correct and then invoke `cb` with a user object, which // will be set at `req.user` in route handlers after authentication. passport.use(new Strategy( function(username, password, cb) { db.users.findByUsername(username, function(err, user) { winston.info('trying to lookup user.'); if (err) { winston.info('db.users.findByUsername error.'); return cb(err); } if (!user) { winston.info('bad user'); return cb(null, false); } if (user.password != password) { winston.info('bad pw'); return cb(null, false); } return cb(null, user); }); })); // Configure Passport authenticated session persistence. // // In order to restore authentication state across HTTP requests, Passport needs // to serialize users into and deserialize users out of the session. The // typical implementation of this is as simple as supplying the user ID when // serializing, and querying the user record by ID from the database when // deserializing. passport.serializeUser(function(user, cb) { cb(null, user.id); }); passport.deserializeUser(function(id, cb) { db.users.findById(id, function (err, user) { if (err) { return cb(err); } cb(null, user); }); }); // Setup email var transporter = nodemailer.createTransport({ service: 'Gmail', auth: { user: 'alertmonitorfl@gmail.com', pass: 'ZPW!KdxkdTSmyH99' } }); var mailOptions = { from: 'Alert Monitor <alertmonitorfl@gmail.com>', //to: 'jody@kaplon.us,don@gettner.com', to: 'jody@kaplon.us', subject: 'Device did not wish me a GoodMorning or GoodEvening', }; var pg = require("pg"); var conString = "postgres://courtsopen:courtsopen@db/courtsopen"; app.use(bodyParser.json()); // Needed for JSON POST requests from Particle Cores. app.use(bodyParser.urlencoded({ extended: false })); // Needed for web POST requests from edit form. // Use `.hbs` for extensions and find partials in `views/partials`. app.engine('hbs', hbs.express4({ partialsDir: __dirname + '/views/partials' })); app.set('view engine', 'hbs'); app.set('views', __dirname + '/views'); var favicon = require('serve-favicon'); app.use(favicon(__dirname + '/assets/favicon.ico')); // Put this before setting static dir. app.use(express.static('assets')); app.use(require('cookie-parser')()); app.use(require('express-session')({ secret: 'keyboard cat', resave: false, saveUninitialized: false })); // Initialize Passport and restore authentication state, if any, from the session. app.use(passport.initialize()); app.use(passport.session()); // As with any middleware it is quintessential to call next() if the user is authenticated var isAuthenticated = function (req, res, next) { var loc = req.params.loc; if (req.isAuthenticated()) { return next(); res.redirect(req); // I think this is what I want, but might cause re-dir loop. } else { // If user NOT authenticated, redirect to login page, do not call next(). res.redirect('/' + loc + '/login'); } } /******************************************************************************************************* With this express setup, ordering of routes matters!!! It's 1st-come-1st-served. If a more specific route is placed after a more general route, the general route will be chosen. Not like nginx where the most specific match always wins. Using express.Router() in express-4 might fix this, as would converting to hapi... But, for now, order routes like this: - static routes, ordered more specific to less specific - dynamic routes, ordered more specific to less specific ********************************************************************************************************/ app.get('/logout', function(req, res){ req.logout(); res.redirect('/'); // possible to go back to appropriate :loc? }); app.get('/', function(req, res){ winston.info("GET /"); res.render('home', {}, function(err, html) { if(err !== null) { winston.error(err); } else { res.send(html); } }); }); app.post('/', function(req, res){ var postEvent = req.body.postEvent; var source = req.body.source; winston.info(req.body); // If it's stripped down JSON sent by cell modem, there won't be a req.body.data key w/full-nested JSON as value. // If post body includes coreid and published_at, assume it's from a particle devcie and parse accordingly. if (req.body.coreid !== undefined && req.body.published_at !== undefined) { var innerDataJSON = JSON.parse(req.body.data); var status = JSON.stringify(innerDataJSON.status, null, 4).slice(1,-1); var coreid = JSON.stringify(req.body.coreid, null, 4).slice(1,-1); var pubAt = JSON.stringify(req.body.published_at, null, 4).slice(1,-1); pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } client.query( "INSERT INTO alerts (origjson, coreid, published_at, status) VALUES ($1, $2, $3, $4);", [JSON.stringify(req.body, null, 4), coreid, pubAt, status] ); done(); }); } else if (req.body.loc !== undefined) { // Assume POST from /admin template var coreid = '2a002b000947343432313031' // Hard-code TT coreid for now, otherwise need to look up coreid from location value. var received_at = new Date(Date.now()); received_at = received_at.toISOString(); var status = req.body.status; winston.info(coreid + '; ' + received_at + '; ' + status + '; ' + JSON.stringify(req.body, null, 4)); pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } client.query( "INSERT INTO alerts (origjson, coreid, published_at, status) VALUES ($1, $2, $3, $4);", [JSON.stringify(req.body, null, 4), coreid, received_at, status] ); done(); }); } else { // parse for minimal data from cell modem. var deviceid = JSON.stringify(req.body.data, null, 4).slice(1,4); var statusCode = JSON.stringify(req.body.data, null, 4).slice(5,7); var statusFromCode; switch (statusCode) { case "01": statusFromCode = "Open"; break; case "02": statusFromCode = "Closed"; break; case "03": statusFromCode = "GoodMorning"; break; case "04": statusFromCode = "GoodEvening"; break; default: statusFromCode = "UnknownStatus" }; var received_at = new Date(Date.now()); received_at = received_at.toISOString(); winston.info(statusFromCode + ' ' + received_at); pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } client.query( "INSERT INTO alerts (origjson, coreid, published_at, status) VALUES ($1, $2, $3, $4);", [JSON.stringify(req.body, null, 4), deviceid, received_at, statusFromCode] ); done(); }); } res.status(204).send('POST received'); }); app.get('/:loc/admin', isAuthenticated, function(req, res) { var loc = req.params.loc; winston.info('GET ' + loc + '/admin'); if (loc !== 'tt') { res.status(404).send('Not found'); } else { // TODO: load admin template //res.status(200).send('admin template here...'); pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } var devIndexQry = "select status, published_at " + "from alerts " + "where status in ('Open', 'Closed') " + "and coreid = '2a002b000947343432313031' " + "order by published_at desc " + "limit 2" client.query(devIndexQry, function(err, result) { //call `done()` to release the client back to the pool done(); if(err) { return winston.error('error running query', err); } // Loop over elements in rows array, convert ugly UTC times to pretty local times. result.rows.forEach(function(row){ row.loc = loc; row.pubdate = courtsopenUtils.getLocDateFromUTC(row.published_at); row.pubtime = courtsopenUtils.getLocTimeFromUTC(row.published_at); if(row.status.toLowerCase().indexOf('closed') > -1){ row.statusclass = 'closed'; row.oppstatus = 'open'; row.prettystatus = 'Open'; } else { row.statusclass = 'open'; row.oppstatus = 'closed'; row.prettystatus = 'Closed'; } }); res.render('admin', {values: result.rows}, function(err, html) { if(err !== null) { winston.error(err); } else { res.send(html); } }); }); }); } }); app.get('/:loc/login', function(req, res){ var loc = req.params.loc; winston.info('GET ' + loc + '/login'); res.render('login', {loc}, function(err, html) { if(err !== null) { winston.error(err); } else { res.send(html); } }); }); app.post('/:loc/login', passport.authenticate('local', { failureRedirect: '/:loc/login' }), function(req, res) { winston.info('sucessful login'); //res.redirect('/:loc/admin'); // no worky, :loc is literal here, not repop by param. //res.redirect(req); // no worky, get https://courts.kaplon.us/[object%20Object] var loc = req.params.loc; res.redirect('/' + loc + '/admin'); // no worky, undefined here...how to include w/post? }); app.get('/:loc/status', function(req, res) { var loc = req.params.loc; if (loc !== 'tt') { res.status(404).send('Not found'); } else { winston.info('GET ' + loc + '/status'); // Lookup most recent status from DB and return as JSON. pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } var mostRecentStatusQry = "select origjson " + "from alerts " + "where status in ('Open', 'Closed') " + "and coreid = '2a002b000947343432313031' " + "order by published_at desc " + "limit 1" client.query(mostRecentStatusQry, function(err, result) { //call `done()` to release the client back to the pool done(); if(err) { return winston.error('error running query', err); } res.setHeader('Content-Type', 'application/json'); res.send(JSON.stringify(result.rows[0].origjson)); }); }); } }); app.get('/:loc', function(req, res) { var loc = req.params.loc; if (loc !== 'tt') { res.status(404).send('Not found'); } else { winston.info("GET /tt"); pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } var devIndexQry = "select status, published_at " + "from alerts " + "where status in ('Open', 'Closed') " + "and coreid = '2a002b000947343432313031' " + "order by published_at desc " + "limit 2" client.query(devIndexQry, function(err, result) { //call `done()` to release the client back to the pool done(); if(err) { return winston.error('error running query', err); } // Loop over elements in rows array, convert ugly UTC times to pretty local times. result.rows.forEach(function(row){ row.pubdate = courtsopenUtils.getLocDateFromUTC(row.published_at); row.pubtime = courtsopenUtils.getLocTimeFromUTC(row.published_at); if(row.status.toLowerCase().indexOf('closed') > -1){ row.statusclass = 'closed'; } else { row.statusclass = 'open'; } }); res.render('location', {values: result.rows}, function(err, html) { if(err !== null) { winston.error(err); } else { res.send(html); } }); }); }); } }); /* // 08.29.2019, disable logging and email since Don turned off device. setInterval(function() { // Check every hour to see if GoodMorning or GoodEvening has gone missing. pg.connect(conString, function(err, client, done) { if(err) { return winston.error('error fetching client from pool', err); } var deadManQry = "select published_at from alerts where to_timestamp(published_at, 'YYYY-MM-DD HH24:MI:SS') > (now() - interval '14.5 hours') limit 1"; client.query(deadManQry, function(err, result) { //call `done()` to release the client back to the pool done(); if(err) { return winston.error('error running query', err); } else if (result.rowCount == 0) { mailOptions.text = "It's been too long since the last data transmission from device. \n\n"; winston.info(mailOptions.text); // Don't include any other details for now, will need to change DB query to get details on last message received. //mailOptions.text = mailOptions.text + 'Status message, ' + status + '\n'; //mailOptions.text = mailOptions.text + 'Published at, ' + courtsopenUtils.getLocDateFromUTC(pubAt) + ' ' + courtsopenUtils.getLocTimeFromUTC(pubAt) + '\n'; transporter.sendMail(mailOptions, function(error, info){ if(error){ winston.error(error); }else{ winston.info('Message sent: ' + info.response); } }); } }); }); }, 60 * 60 * 1000); */ app.listen(3000, function() { winston.info("Started on PORT 3000"); });