Files
2025-08-07 21:15:07 +01:00

917 lines
23 KiB
JavaScript

var fs = require('fs');
var url = require('url');
var rfs = require('rotating-file-stream');
var mqtt = require('mqtt');
var path = require('path');
var http = require('http');
var https = require('https');
var flash = require('connect-flash');
var morgan = require('morgan');
var express = require('express');
var session = require('express-session');
var passport = require('passport');
var mongoose = require('mongoose');
var bodyParser = require('body-parser');
var Measurement = require('./googleMeasurement.js');
var cookieParser = require('cookie-parser');
var BasicStrategy = require('passport-http').BasicStrategy;
var LocalStrategy = require('passport-local').Strategy;
var SimpleNodeLogger = require('simple-node-logger');
var PassportOAuthBearer = require('passport-http-bearer');
var logDirectory = path.join(__dirname, 'log');
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);
var loggingOptions = {
logDirectory: 'log',
fileNamePattern:'debug-<DATE>.log',
timestampFormat:'YYYY-MM-DD HH:mm:ss.SSS',
dateFormat:'YYYY.MM.DD'
};
var logger = SimpleNodeLogger.createRollingFileLogger(loggingOptions);
var oauthServer = require('./oauth');
var port = (process.env.VCAP_APP_PORT || process.env.PORT || 3000);
var host = (process.env.VCAP_APP_HOST || '0.0.0.0');
var mongo_url = (process.env.MONGO_URL || 'mongodb://localhost/users');
var mqtt_url = (process.env.MQTT_URL || 'mqtt://localhost:1883');
var mqtt_user = (process.env.MQTT_USER || undefined);
var mqtt_password = (process.env.MQTT_PASSWORD || undefined);
console.log(mqtt_url);
var googleAnalyicsTID = process.env.GOOGLE_ANALYTICS_TID;
var measurement = new Measurement(googleAnalyicsTID);
var mqttClient;
var mqttOptions = {
reconnectPeriod: 3000,
keepAlive: 10,
clean: true,
clientId: 'webApp_' + Math.random().toString(16).substr(2, 8)
};
if (mqtt_user) {
mqttOptions.username = mqtt_user;
mqttOptions.password = mqtt_password;
}
mqttClient = mqtt.connect(mqtt_url, mqttOptions);
mqttClient.on('error',function(err){
});
mqttClient.on('reconnect', function(){
});
mqttClient.on('connect', function(){
mqttClient.subscribe('response/#');
});
if (process.env.VCAP_SERVICES) {
var services = JSON.parse(process.env.VCAP_SERVICES);
for (serviceName in services) {
if (serviceName.match('^mongo')) {
var creds = services[serviceName][0]['credentials'];
mongo_url = creds.url;
} else {
console.log("no database found in vcap_services");
}
}
}
console.log(mongo_url);
mongoose.Promise = global.Promise;
var mongoose_options = {
server: {
auto_reconnect:true,
autoReconnect: true,
reconnectTries: Number.MAX_VALUE,
reconnectInterval: 1000,
socketOptions: {
autoReconnect: true
}
}
};
var mongoose_connection = mongoose.connection;
mongoose_connection.on('connecting', function() {
console.log('connecting to MongoDB...');
});
mongoose_connection.on('error', function(error) {
console.error('Error in MongoDb connection: ' + error);
//mongoose.disconnect();
});
mongoose_connection.on('connected', function() {
console.log('MongoDB connected!');
});
mongoose_connection.once('open', function() {
console.log('MongoDB connection opened!');
});
mongoose_connection.on('reconnected', function () {
console.log('MongoDB reconnected!');
});
mongoose_connection.on('disconnected', function() {
console.log('MongoDB disconnected!');
});
mongoose.connect(mongo_url, mongoose_options);
var Account = require('./models/account');
var oauthModels = require('./models/oauth');
var Devices = require('./models/devices');
var Topics = require('./models/topics');
var LostPassword = require('./models/lostPassword');
Account.findOne({username: mqtt_user}, function(error, account){
if (!error && !account) {
Account.register(new Account({username: mqtt_user, email: '', mqttPass: '', superuser: 1}),
mqtt_password, function(err, account){
var topics = new Topics({topics: [
'command/' +account.username+'/#',
'presence/' + account.username + '/#',
'response/' + account.username + '/#'
]});
topics.save(function(err){
if (!err){
var s = Buffer.from(account.salt, 'hex').toString('base64');
var h = Buffer.from(account.hash, 'hex').toString(('base64'));
var mqttPass = "PBKDF2$sha256$901$" + account.salt + "$" + account.hash;
Account.update(
{username: account.username},
{$set: {mqttPass: mqttPass, topics: topics._id}},
{ multi: false },
function(err, count){
if (err) {
logger.info("error adding MQTT account\n", err);
console.log(err);
}
}
);
}
});
});
} else {
console.log("MQTT account already exists");
}
});
var app_id = 'http://localhost:' + port;
// if (process.env.VCAP_APPLICATION) {
// var application = JSON.parse(process.env.VCAP_APPLICATION);
// var app_uri = application['application_uris'][0];
// app_id = 'https://' + app_uri;
// }
var cookieSecret = 'ihytsrf334';
var accessLogStream = rfs('access.log', {
interval: '1d', // rotate daily
compress: 'gzip', // compress rotated files
maxFiles: 30,
path: logDirectory
});
var app = express();
app.set('view engine', 'ejs');
app.enable('trust proxy');
app.use(morgan("combined", {stream: accessLogStream}));
app.use(cookieParser(cookieSecret));
app.use(flash());
app.use(session({
// genid: function(req) {
// return genuuid() // use UUIDs for session IDs
// },
secret: cookieSecret,
resave: false,
saveUninitialized: false,
cookie: {
//secure: true
}
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(passport.initialize());
app.use(passport.session());
function requireHTTPS(req, res, next) {
if (req.get('X-Forwarded-Proto') === 'http') {
//FYI this should work for local development as well
var url = 'https://' + req.get('host');
if (req.get('host') === 'localhost') {
url += ':' + port;
}
url += req.url;
return res.redirect(url);
}
next();
}
app.use(requireHTTPS);
app.use('/',express.static('static'));
passport.use(new LocalStrategy(Account.authenticate()));
passport.use(new BasicStrategy(Account.authenticate()));
passport.serializeUser(Account.serializeUser());
passport.deserializeUser(Account.deserializeUser());
var accessTokenStrategy = new PassportOAuthBearer(function(token, done) {
oauthModels.AccessToken.findOne({ token: token }).populate('user').populate('grant').exec(function(error, token) {
if (!error && token) {
logger.info("db token: ", token.active);
logger.info("db token.grant : ", token.grant.active);
logger.info("db token.user: ", token.user);
}
if (!error && token && token.active && token.grant && token.grant.active && token.user) {
// console.log("Token is GOOD!");
done(null, token.user, { scope: token.scope });
} else if (!error) {
// console.log("TOKEN PROBLEM");
done(null, false);
} else {
// console.log("TOKEN PROBLEM 2");
done(error);
}
});
});
passport.use(accessTokenStrategy);
app.get('/', function(req,res){
res.render('pages/index', {user: req.user, home: true});
});
app.get('/docs', function(req,res){
res.render('pages/docs', {user: req.user, docs: true});
});
app.get('/about', function(req,res){
res.render('pages/about', {user: req.user, about: true});
});
app.get('/login', function(req,res){
res.render('pages/login',{user: req.user, message: req.flash('error')});
});
app.get('/logout', function(req,res){
req.logout();
if (req.query.next) {
//console.log(req.query.next);
res.redirect(req.query.next);
} else {
res.redirect('/');
}
});
//app.post('/login',passport.authenticate('local', { failureRedirect: '/login', successRedirect: '/2faCheck', failureFlash: true }));
app.post('/login',
passport.authenticate('local',{ failureRedirect: '/login', failureFlash: true, session: true }),
function(req,res){
//console.log("login success");
//console.log(req.isAuthenticated());
//console.log(req.user);
if (req.query.next) {
res.reconnect(req.query.next);
} else {
//console.log("passed Auth");
res.redirect('/devices');
}
});
function ensureAuthenticated(req,res,next) {
//console.log("ensureAuthenticated - %j", req.isAuthenticated());
//console.log("ensureAuthenticated - %j", req.user);
//console.log("ensureAuthenticated - %j", req.session);
if (req.isAuthenticated()) {
return next();
} else {
//console.log("failed auth?");
res.redirect('/login');
}
}
app.get('/newuser', function(req,res){
res.render('pages/register',{user: req.user});
});
app.post('/newuser', function(req,res){
Account.register(new Account({ username : req.body.username, email: req.body.email, mqttPass: "foo" }), req.body.password, function(err, account) {
if (err) {
logger.info("new user problem\n", err);
console.log(err);
return res.status(400).send(err.message);
}
var topics = new Topics({topics: [
'command/' + account.username +'/#',
'presence/'+ account.username + '/#',
'response/' + account.username + '/#'
]});
topics.save(function(err){
if (!err){
var s = Buffer.from(account.salt, 'hex').toString('base64');
var h = Buffer.from(account.hash, 'hex').toString(('base64'));
var mqttPass = "PBKDF2$sha256$901$" + account.salt + "$" + account.hash;
Account.update(
{username: account.username},
{$set: {mqttPass: mqttPass, topics: topics._id}},
{ multi: false },
function(err, count){
if (err) {
console.log(err);
}
}
);
}
});
passport.authenticate('local')(req, res, function () {
logger.info("created new user ", req.body.username);
console.log("created new user %s", req.body.username);
measurement.send({
t:'event',
ec:'System',
ea: 'NewUser',
uid: req.body.username
});
res.status(201).send();
});
});
});
app.get('/changePassword/:key',function(req, res, next){
var uuid = req.params.key;
LostPassword.findOne({uuid: uuid}).populate('user').exec(function(error, lostPassword){
if (!error && lostPassword) {
req.login(lostPassword.user, function(err){
if (!err){
// lostPassword.remove();
res.redirect('/changePassword');
} else {
console.log(err);
res.redirect('/');
}
})
} else {
res.redirect('/');
}
});
});
app.get('/changePassword', ensureAuthenticated, function(req, res, next){
res.render('pages/changePassword', {user: req.user});
});
app.post('/changePassword', ensureAuthenticated, function(req, res, next){
Account.findOne({username: req.user.username}, function (err, user){
if (!err && user) {
user.setPassword(req.body.password, function(e,u){
// var s = Buffer.from(account.salt, 'hex').toString('base64');
// var h = Buffer.from(account.hash, 'hex').toString(('base64'));
var mqttPass = "PBKDF2$sha256$901$" + user.salt + "$" + user.hash;
u.mqttPass = mqttPass;
u.save(function(error){
if (!error) {
//console.log("Chagned %s's password", u.username);
LostPassword.findOne({user: user._id}).exec(function(e,lp){
if (!e && lp) {
lp.remove()
}
})
res.status(200).send();
} else {
logger.info("Error changing ", u.username, "'s password");
console.log("Error changing %s's password", u.username);
console.log(error);
res.status(400).send("Problem setting new password");
}
});
});
} else {
console.log(err);
res.status(400).send("Problem setting new password");
}
});
});
app.get('/lostPassword', function(req, res, next){
res.render('pages/lostPassword', { user: req.user});
});
var sendemail = require('./sendemail');
var mailer = new sendemail();
app.post('/lostPassword', function(req, res, next){
var email = req.body.email;
Account.findOne({email: email}, function(error, user){
if (!error){
if (user){
var lostPassword = new LostPassword({user: user});
//console.log(lostPassword);
lostPassword.save(function(err){
if (!err) {
res.status(200).send();
}
//console.log(lostPassword.uuid);
//console.log(lostPassword.user.username);
var body = mailer.buildLostPasswordBody(lostPassword.uuid, lostPassword.user.username);
mailer.send(email, 'alexa-node-red@hardill.me.uk', 'Password Reset for Alexa Node-RED', body.text, body.html);
});
} else {
res.status(404).send("No user found with that email address");
}
}
});
});
app.get('/auth/start',oauthServer.authorize(function(applicationID, redirectURI, done) {
oauthModels.Application.findOne({ oauth_id: applicationID }, function(error, application) {
if (application) {
var match = false, uri = url.parse(redirectURI || '');
for (var i = 0; i < application.domains.length; i++) {
if (uri.host == application.domains[i] || (uri.protocol == application.domains[i] && uri.protocol != 'http' && uri.protocol != 'https')) {
match = true;
break;
}
}
if (match && redirectURI && redirectURI.length > 0) {
done(null, application, redirectURI);
} else {
done(new Error("You must supply a redirect_uri that is a domain or url scheme owned by your app."), false);
}
} else if (!error) {
done(new Error("There is no app with the client_id you supplied."), false);
} else {
done(error);
}
});
}),function(req,res){
var scopeMap = {
// ... display strings for all scope variables ...
access_devices: 'access you devices',
create_devices: 'create new devices'
};
res.render('pages/oauth', {
transaction_id: req.oauth2.transactionID,
currentURL: encodeURIComponent(req.originalUrl),
response_type: req.query.response_type,
errors: req.flash('error'),
scope: req.oauth2.req.scope,
application: req.oauth2.client,
user: req.user,
map: scopeMap
});
});
app.post('/auth/finish',function(req,res,next) {
//console.log("/auth/finish user: ", req.user);
//console.log(req.body);
//console.log(req.params);
if (req.user) {
next();
} else {
passport.authenticate('local', {
session: false
}, function(error,user,info){
//console.log("/auth/finish authenting");
if (user) {
//console.log(user.username);
req.user = user;
next();
} else if (!error){
//console.log("not authed");
req.flash('error', 'Your email or password was incorrect. Please try again.');
res.redirect(req.body['auth_url'])
}
})(req,res,next);
}
}, oauthServer.decision(function(req,done){
//console.log("decision user: ", req);
done(null, { scope: req.oauth2.req.scope });
}));
app.post('/auth/exchange',function(req,res,next){
var appID = req.body['client_id'];
var appSecret = req.body['client_secret'];
oauthModels.Application.findOne({ oauth_id: appID, oauth_secret: appSecret }, function(error, application) {
if (application) {
req.appl = application;
next();
} else if (!error) {
error = new Error("There was no application with the Application ID and Secret you provided.");
next(error);
} else {
next(error);
}
});
}, oauthServer.token(), oauthServer.errorHandler());
app.get('/api/v1/devices',
passport.authenticate(['bearer', 'basic'], { session: false }),
function(req,res,next){
//console.log("all good, doing discover devices");
measurement.send({
t:'event',
ec:'discover',
ea: req.body.header ? req.body.header.name : "Node-RED",
uid: req.user.username
});
var user = req.user.username
Devices.find({username: user},function(error, data){
if (!error) {
var devs = [];
for (var i=0; i< data.length; i++) {
var dev = {};
dev.friendlyName = data[i].friendlyName;
dev.friendlyDescription = data[i].friendlyDescription;
dev.applianceId = "" + data[i].applianceId;
dev.isReachable = data[i].isReachable;
dev.actions = data[i].actions;
dev.applianceTypes = data[i].applianceTypes;
dev.additionalApplianceDetails = data[i].additionalApplianceDetails;
dev.modelName = "Node-RED Endpoint";
dev.version = "0.0.1";
dev.manufacturerName = "Node-RED"
devs.push(dev);
}
logger.info(user , " discovering " , JSON.stringify(devs) );
//console.log(devs)
res.send(devs);
}
});
}
);
var onGoingCommands = {};
mqttClient.on('message',function(topic,message){
if (topic.startsWith('response/')){
var payload;
try {
payload = JSON.parse(message.toString());
} catch (errr) {
console.log(errr);
return;
}
var waiting = onGoingCommands[payload.messageId];
if (waiting) {
//console.log("mqtt response: " + JSON.stringify(payload,null," "));
logger.info("mqtt response (", waiting.user , ") - ", JSON.stringify(payload,null," "))
if (payload.success) {
waiting.res.status(200);
if (payload.extra) {
if (typeof payload.extra == "object") {
logger.info("extra payload", payload.extra);
waiting.res.send(payload.extra);
} else {
logger.info("extra not an object: ", payload);
logger.info(waiting.user);
waiting.res.send({});
}
} else {
//console.log("not sending extra");
waiting.res.send({});
}
} else {
if (payload.extra && payload.extra.min) {
//console.log("out of range");
waiting.res.status(416).send(payload.extra);
} else {
//console.log("malfunction");
waiting.res.status(503).send();
}
}
delete onGoingCommands[payload.messageId];
// should really parse uid out of topic
measurement.send({
t:'event',
ec:'command',
ea: 'complete',
uid: waiting.user
});
}
}
});
var timeout = setInterval(function(){
var now = Date.now();
var keys = Object.keys(onGoingCommands);
for (key in keys){
var waiting = onGoingCommands[keys[key]];
//console.log(keys[key]);
if (waiting) {
var diff = now - waiting.timestamp;
if (diff > 6000) {
//console.log("timed out");
logger.info("timed out - ", waiting.user);
waiting.res.status(504).send('{"error": "timeout"}');
delete onGoingCommands[keys[key]];
measurement.send({
t:'event',
ec:'command',
ea: 'timeout',
uid: waiting.user
});
}
}
}
},500);
app.post('/api/v1/command',
passport.authenticate('bearer', { session: false }),
function(req,res,next){
//console.log(req.user.username);
//console.log(req.body);
measurement.send({
e:'event',
ec:'command',
ea: req.body.header.name,
uid: req.user.username
});
var topic = "command/" +req.user.username + "/" + req.body.payload.appliance.applianceId;
delete req.body.payload.accessToken;
var message = JSON.stringify(req.body);
try{
mqttClient.publish(topic,message);
} catch (err) {
}
var command = {
user: req.user.username,
res: res,
timestamp: Date.now()
};
onGoingCommands[req.body.header.messageId] = command;
}
);
app.get('/devices',
ensureAuthenticated,
function(req,res){
var user = req.user.username;
Devices.find({username:user}, function(err, data){
if (!err) {
//console.log(data);
res.render('pages/devices',{user: req.user ,devices: data, devs: true});
}
});
});
app.put('/devices',
ensureAuthenticated,
function(req,res){
var user = req.user.username;
var device = req.body;
device.username = user;
device.isReachable = true;
var dev = new Devices(device);
dev.save(function(err, dev){
if (!err) {
res.status(201)
res.send(dev);
} else {
res.status(500);
res.send(err);
}
});
});
app.post('/device/:dev_id',
ensureAuthenticated,
function(req,res){
var user = req.user.username;
var id = req.params.dev_id;
var device = req.body;
if (user === device.username) {
Devices.findOne({_id: device._id, username: device.username},
function(err, data){
if (err) {
res.status(500);
res.send(err);
} else if (data) {
data.friendlyDescription = device.friendlyDescription;
data.actions = device.actions;
data.applianceTypes = device.applianceTypes;
data.save(function(err, d){
res.status(201);
res.send(d);
});
} else {
res.status(500);
res.send("No such device");
logger.info("No such device");
logger.info("username: ", user);
logger.info("device_id: ", id);
}
});
}
});
app.delete('/device/:dev_id',
ensureAuthenticated,
function(req,res){
var user = req.user.username;
var id = req.params.dev_id;
console.log(id);
Devices.remove({_id: id, username: user},
function(err) {
if (err) {
console.log(err);
res.status(500);
res.send(err);
} else {
res.status(202);
res.send();
}
});
});
app.post('/api/v1/devices',
passport.authenticate('bearer', { session: false }),
function(req,res,next){
var devices = req.body;
if (typeof devices == 'object' && Array.isArray(devices)) {
for (var i=0; i<devices.lenght; i++) {
var applianceId = devices[i].applianceId;
Devices.update({
username: req.user,
applianceId: applianceId
},
devices[i],
{
upsert: true
},
function(err){
//log error
});
}
} else {
res.error(400);
}
}
);
app.get('/admin',
ensureAuthenticated,
function(req,res){
if (req.user.username == mqtt_user) {
Account.count({},function(err, count){
res.render('pages/admin',{user:req.user, userCount: count});
});
} else {
res.redirect('/');
}
});
app.get('/admin/services',
ensureAuthenticated,
function(req,res){
if (req.user.username === mqtt_user) {
oauthModels.Application.find({}, function(error, data){
if (!error){
res.render('pages/services',{user:req.user, services: data});
}
});
} else {
res.status(401).send();
}
});
app.get('/admin/users',
ensureAuthenticated,
function(req,res){
if (req.user.username === mqtt_user) {
Account.find({}, function(error, data){
res.send(data);
});
} else {
res.status(401).send();
}
});
app.get('/admin/devices',
ensureAuthenticated,
function(req,res){
if (req.user.username === mqtt_user) {
Devices.find({},function(error, data){
res.send(data);
});
} else {
res.status(401).send();
}
});
app.put('/services',
ensureAuthenticated,
function(req,res){
if (req.user.username == mqtt_user) {
var application = oauthModels.Application(req.body);
application.save(function(err, application){
if (!err) {
res.status(201).send(application);
}
});
} else {
res.status(401).send();
}
});
app.post('/service/:id',
ensureAuthenticated,
function(req,res){
var service = req.body;
oauthModels.Application.findOne({_id: req.params.id},
function(err,data){
if (err) {
res.status(500);
res.send(err);
} else {
data.title = service.title;
data.oauth_secret = service.oauth_secret;
data.domains = service.domains;
data.save(function(err, d){
res.status(201);
res.send(d);
});
}
});
});
app.delete('/service/:id',
ensureAuthenticated,
function(req,res){
oauthModels.Application.remove({_id:req.params.id},
function(err){
if (!err) {
res.status(200).send();
} else {
res.status(500).send();
}
});
});
var server = http.Server(app);
if (app_id.match(/^https:\/\/localhost:/)) {
var options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.crt')
};
server = https.createServer(options, app);
}
server.listen(port, host, function(){
console.log('App listening on %s:%d!', host, port);
console.log("App_ID -> %s", app_id);
setTimeout(function(){
},5000);
});