Browse Source

Merge pull request #2 from naresh97/cleanup/break-apart-app-js

Cleanup/break apart app js
feature/informcontacts
Nareshkumar Rao 3 years ago
committed by GitHub
parent
commit
303d3ba43a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      .eslintrc.js
  2. 1
      .prettierrc.json
  3. 7
      README.md
  4. 306
      app.js
  5. 1
      app.yaml
  6. 2013
      package-lock.json
  7. 8
      package.json
  8. 30
      src/app.js
  9. 83
      src/db/db.js
  10. 47
      src/db/utils.js
  11. 47
      src/routes/CodeRoute.js
  12. 71
      src/routes/LoginRoute.js
  13. 41
      src/routes/VerifyRoute.js
  14. 18
      src/session.js
  15. 38
      src/telegram.js
  16. 5
      src/utils.js

12
.eslintrc.js

@ -0,0 +1,12 @@
module.exports = {
env: {
node: true,
commonjs: true,
es2021: true,
},
extends: "eslint:recommended",
parserOptions: {
ecmaVersion: 12,
},
rules: {},
};

1
.prettierrc.json

@ -0,0 +1 @@
{}

7
README.md

@ -1,14 +1,19 @@
# SSR Tracing Backend
This is the backend API provider for the [SSR Tracing Web-App](https://github.com/naresh97/ssr-tracing).
## Development
### Prerequisites
- NodeJS
- NPM
### Building
- Run `npm install` to install the required packages
- Run `nodejs ./app.js` to start the server
### .env
Server parameters are specified in the *.env* file, see *.env.template* for an example
Server parameters are specified in the _.env_ file, see _.env.template_ for an example

306
app.js

@ -1,306 +0,0 @@
const express = require('express');
const { Sequelize, DataTypes, STRING } = require('sequelize');
const session = require('express-session');
const bcrypt = require('bcrypt');
const QRCode = require('qrcode');
const cors = require('cors');
const { createSecureServer } = require('http2');
const { default: axios } = require('axios');
require("dotenv-flow").config();
const crypto = require('crypto');
var SequelizeStore = require("connect-session-sequelize")(session.Store);
const COOKIE_EXPIRY_DURATION = 60 * 60 * 3 * 1000; // 3 hours in milliseconds
const isProduction = process.env.NODE_ENV == "production";
const isDev = process.env.NODE_ENV == "development";
console.log(`Node Environment: ${process.env.NODE_ENV}`);
const sequelize = (() => {
if (isProduction) {
return new Sequelize(process.env.DB_DATA_NAME, process.env.DB_USER, process.env.DB_PASS, {
host: process.env.DB_PATH,
dialect: process.env.DB_DATA_DIALECT,
});
} else {
return new Sequelize('sqlite::memory:');
}
})();
const storeDB = (() => {
if (isProduction) {
return new Sequelize(process.env.DB_STORE_NAME, process.env.DB_USER, process.env.DB_PASS, {
host: process.env.DB_PATH,
dialect: process.env.DB_DATA_DIALECT,
});
} else {
return new Sequelize('sqlite::memory:');
}
})();
const store = new SequelizeStore({
db: storeDB,
expiration: COOKIE_EXPIRY_DURATION,
});
store.sync();
const Contact = sequelize.define('Contact', {
user: {
type: DataTypes.INTEGER,
allowNull: false,
},
with: {
type: DataTypes.INTEGER,
allowNull: false,
},
});
Contact.sync();
const User = sequelize.define('User', {
telegram: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
},
verification: {
type: DataTypes.STRING,
},
});
User.sync().then(() => {
if (process.env.ADMIN_USERNAME && process.env.ADMIN_PASSWORD) {
User.create({
telegram: process.env.ADMIN_USERNAME,
hash: bcrypt.hashSync(process.env.ADMIN_PASSWORD, 10),
}).catch(e => {
console.log("Couldn't create admin account. Probably exists.");
});
}
});
function authUser(telegramResponse, done) {
let dataCheckArray = [];
for (const [key, value] of Object.entries(telegramResponse)) {
if (key == "hash") continue;
dataCheckArray.push(`${key}=${value}`);
}
dataCheckArray.sort();
const dataCheckString = dataCheckArray.join("\n");
secretKey = crypto.createHash("sha256").update(process.env.TELEGRAM_TOKEN).digest();
confirmationHash = crypto.createHmac("sha256", secretKey).update(dataCheckString).digest("hex");
const authorized = confirmationHash == telegramResponse.hash;
if (!authorized) {
done({ authorized: false });
}
User.findOne({
where: {
telegram: telegramResponse.id,
}
}).then(user => {
if (!user) {
createUser(telegramResponse.id, (success) => {
done({ authorized: success });
})
} else {
done({ authorized: true });
}
});
}
function refreshVerification(user, done) {
let newVerification = bcrypt.hashSync(`${new Date().getTime()}-${user.hash}`, 5).replace(/[^a-zA-Z0-9]+/g, "");
newVerification = newVerification.substr(0, newVerification.length / 2);
user.verification = newVerification;
user.save().then(result => {
done(result)
});
}
function createQRCode(telegram, done) {
User.findOne({
where: {
telegram: telegram
}
})
.then(user => {
refreshVerification(user, result => {
const verifyURL = `${process.env.WEBSITE_URL}/#/verify/${encodeURIComponent(result.verification)}`;
QRCode.toDataURL(verifyURL, { width: 300, height: 300 }, (err, url) => {
done(err, url);
})
});
})
.catch(err => {
done(err);
});
}
function checkVerification(id, done) {
User.findOne({
where: {
verification: decodeURIComponent(id),
}
}).then(user => {
if (user) {
done(true, "User verified", user.id);
} else {
done(false, "No such verification");
}
});
}
function createUser(telegram, done) {
User.create({
telegram: telegram
}).then(user => {
if (!user) {
done(false, "Could not create user");
} else {
done(true, "Success");
}
}).catch(reason => {
if (reason.name == "SequelizeUniqueConstraintError") {
done(false, "User already exists");
} else {
done(false, "Unknown error");
}
});
}
function addContact(telegram, withUserID, done) {
User.findOne({ where: { telegram: telegram } }).then(user => {
User.findOne({ where: { id: withUserID } }).then(withUser => {
Contact.create({ user: user.id, with: withUserID })
.then(res => {
console.log(`Registering contact between ${user.id} and ${withUserID}`);
sendTelegramMessage(withUser.telegram,
"Someone scanned your QR code. You will be notified if they are tested positive with Covid. If you are tested positive, please tell this bot /COVIDPOSITIVE",
() => {}
);
done(true, "Successfully added contact");
})
.catch(e => {
done(false, e);
});
})
})
}
function getCookieExpiry() {
return new Date(Date.now() + COOKIE_EXPIRY_DURATION);
}
function setTelegramWebHook(done) {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/setWebhook`;
axios.post(url, {
url: `${process.env.SERVER_API_URL}/${process.env.TELEGRAM_SECRET}`,
allowed_updates: [],
drop_pending_updates: true,
})
.then(res => { done(res) })
.catch(err => { done(err) });
}
function sendTelegramMessage(telegramID, message, done) {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`;
axios.post(url, {
chat_id: telegramID,
text: message,
})
.then((res) => {
done(res)
})
.catch(err => {
console.error("Problem sending Telegram message.");
done(err)
});
}
const app = express();
app.set('trust proxy', 1)
app.use(session({
secret: process.env.SERVER_SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
sameSite: "none",
},
store: store,
}))
app.use(cors({ credentials: true, origin: true, secure: true }));
app.use(express.json())
setTelegramWebHook(() => {});
app.post(`/${process.env.TELEGRAM_SECRET}`, (req, res) => {
res.send();
});
app.post('/login', (req, res) => {
telegramResponse = req.body.telegramResponse;
const auth = authUser(telegramResponse, (success, msg) => {
if (success) {
const verified = req.session.verified;
const verifiedBy = req.session.verifiedBy;
req.session.regenerate(() => {
cookieExpiry = getCookieExpiry();
req.session.user = telegramResponse.id;
if (verified) {
addContact(telegramResponse.id, verifiedBy, (contactSuccess) => {
res.send({ authorized: success, message: msg, contactSuccess: contactSuccess });
});
} else {
res.send({ authorized: success, message: msg });
}
});
} else {
res.status(401).send({ authorized: success, message: msg });
}
});
});
app.get('/code', (req, res) => {
if (!req.session.user) {
res.status(401).send("Not logged in");
return;
}
createQRCode(req.session.user, (err, url) => {
res.status(url ? 200 : 401).send({ error: err, data: url });
});
})
app.post("/verify", (req, res) => {
checkVerification(req.body.id, (success, msg, withUserID) => {
cookieExpiry = getCookieExpiry();
req.session.verified = success;
req.session.verifiedBy = withUserID;
if (success) {
if (req.session.user) { // If Logged In
addContact(req.session.user, withUserID, (success, msg) => {
res.status(success ? 200 : 400).send({ success: success, message: msg, loggedIn: true });
});
} else { // If Not Logged In
res.send({ success: success, message: msg, loggedIn: false })
}
} else {
res.status(400).send({ success: success, message: msg });
}
});
});
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
})

1
app.yaml

@ -1,2 +1 @@
runtime: nodejs12

2013
package-lock.json

File diff suppressed because it is too large

8
package.json

@ -2,10 +2,10 @@
"name": "ssr-tracing-backend",
"version": "1.0.0",
"description": "",
"main": "app.js",
"main": "src/app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
"start": "node src/app.js"
},
"author": "",
"license": "ISC",
@ -21,5 +21,9 @@
"qrcode": "^1.4.4",
"sequelize": "^6.6.5",
"sqlite3": "^5.0.2"
},
"devDependencies": {
"eslint": "^7.31.0",
"prettier": "2.3.2"
}
}

30
src/app.js

@ -0,0 +1,30 @@
const express = require("express");
const session = require("express-session");
const cors = require("cors");
require("dotenv-flow").config();
const { LoginRoute } = require("./routes/LoginRoute");
const { CodeRoute } = require("./routes/CodeRoute");
const { VerifyRoute } = require("./routes/VerifyRoute");
const { corsOpts, sessionOpts } = require("./session");
console.log(`Node Environment: ${process.env.NODE_ENV}`);
const app = express();
app.set("trust proxy", 1);
app.use(session(sessionOpts));
app.use(cors(corsOpts));
app.use(express.json());
app.post(`/${process.env.TELEGRAM_SECRET}`, (req, res) => {
res.send();
});
app.post("/login", LoginRoute);
app.get("/code", CodeRoute);
app.post("/verify", VerifyRoute);
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

83
src/db/db.js

@ -0,0 +1,83 @@
const session = require("express-session");
const { Sequelize, DataTypes } = require("sequelize");
var SequelizeStore = require("connect-session-sequelize")(session.Store);
const isProduction = process.env.NODE_ENV == "production";
const sequelize = (() => {
if (isProduction) {
return new Sequelize(
process.env.DB_DATA_NAME,
process.env.DB_USER,
process.env.DB_PASS,
{
host: process.env.DB_PATH,
dialect: process.env.DB_DATA_DIALECT,
}
);
} else {
return new Sequelize("sqlite::memory:");
}
})();
const storeDB = (() => {
if (isProduction) {
return new Sequelize(
process.env.DB_STORE_NAME,
process.env.DB_USER,
process.env.DB_PASS,
{
host: process.env.DB_PATH,
dialect: process.env.DB_DATA_DIALECT,
}
);
} else {
return new Sequelize("sqlite::memory:");
}
})();
const store = new SequelizeStore({
db: storeDB,
});
const Contact = sequelize.define("Contact", {
user: {
type: DataTypes.INTEGER,
allowNull: false,
},
with: {
type: DataTypes.INTEGER,
allowNull: false,
},
});
const User = sequelize.define("User", {
telegram: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
},
verification: {
type: DataTypes.STRING,
},
});
Contact.sync();
User.sync().then(() => {
if (process.env.ADMIN_USERNAME && process.env.ADMIN_PASSWORD) {
User.create({
telegram: process.env.ADMIN_USERNAME,
}).catch(() => {
console.log("Couldn't create admin account. Probably exists.");
});
}
});
store.sync();
exports.User = User;
exports.Contact = Contact;
exports.sequelize = sequelize;
exports.storeDB = storeDB;
exports.store = store;

47
src/db/utils.js

@ -0,0 +1,47 @@
const { sendTelegramMessage } = require("../telegram");
const { User, Contact } = require("./db");
function addContact(telegram, withUserID, done) {
User.findOne({ where: { telegram: telegram } }).then((user) => {
User.findOne({ where: { id: withUserID } }).then((withUser) => {
Contact.create({ user: user.id, with: withUserID })
.then(() => {
console.log(
`Registering contact between ${user.id} and ${withUserID}`
);
sendTelegramMessage(
withUser.telegram,
"Someone scanned your QR code. You will be notified if they are tested positive with Covid. If you are tested positive, please tell this bot /COVIDPOSITIVE",
() => {}
);
done(true, "Successfully added contact");
})
.catch((e) => {
done(false, e);
});
});
});
}
function createUser(telegram, done) {
User.create({
telegram: telegram,
})
.then((user) => {
if (!user) {
done(false, "Could not create user");
} else {
done(true, "Success");
}
})
.catch((reason) => {
if (reason.name == "SequelizeUniqueConstraintError") {
done(false, "User already exists");
} else {
done(false, "Unknown error");
}
});
}
exports.addContact = addContact;
exports.createUser = createUser;

47
src/routes/CodeRoute.js

@ -0,0 +1,47 @@
const bcrypt = require("bcrypt");
const QRCode = require("qrcode");
const { User } = require("../db/db");
function CodeRoute(req, res) {
if (!req.session.user) {
res.status(401).send("Not logged in");
return;
}
createQRCode(req.session.user, (err, url) => {
res.status(url ? 200 : 401).send({ error: err, data: url });
});
}
function createQRCode(telegram, done) {
User.findOne({
where: {
telegram: telegram,
},
})
.then((user) => {
refreshVerification(user, (result) => {
const verifyURL = `${
process.env.WEBSITE_URL
}/#/verify/${encodeURIComponent(result.verification)}`;
QRCode.toDataURL(verifyURL, { width: 300, height: 300 }, (err, url) => {
done(err, url);
});
});
})
.catch((err) => {
done(err);
});
}
function refreshVerification(user, done) {
let newVerification = bcrypt
.hashSync(`${new Date().getTime()}-${user.hash}`, 5)
.replace(/[^a-zA-Z0-9]+/g, "");
newVerification = newVerification.substr(0, newVerification.length / 2);
user.verification = newVerification;
user.save().then((result) => {
done(result);
});
}
exports.CodeRoute = CodeRoute;

71
src/routes/LoginRoute.js

@ -0,0 +1,71 @@
const crypto = require("crypto");
const { User } = require("../db/db");
const { addContact, createUser } = require("../db/utils");
function LoginRoute(req, res) {
const telegramResponse = req.body.telegramResponse;
authUser(telegramResponse, (success, msg) => {
if (success) {
const verified = req.session.verified;
const verifiedBy = req.session.verifiedBy;
req.session.regenerate(() => {
req.session.user = telegramResponse.id;
if (verified) {
addContact(telegramResponse.id, verifiedBy, (contactSuccess) => {
res.send({
authorized: success,
message: msg,
contactSuccess: contactSuccess,
});
});
} else {
res.send({ authorized: success, message: msg });
}
});
} else {
res.status(401).send({ authorized: success, message: msg });
}
});
}
function authUser(telegramResponse, done) {
let dataCheckArray = [];
for (const [key, value] of Object.entries(telegramResponse)) {
if (key == "hash") continue;
dataCheckArray.push(`${key}=${value}`);
}
dataCheckArray.sort();
const dataCheckString = dataCheckArray.join("\n");
const secretKey = crypto
.createHash("sha256")
.update(process.env.TELEGRAM_TOKEN)
.digest();
const confirmationHash = crypto
.createHmac("sha256", secretKey)
.update(dataCheckString)
.digest("hex");
const authorized = confirmationHash == telegramResponse.hash;
if (!authorized) {
done({ authorized: false });
}
User.findOne({
where: {
telegram: telegramResponse.id,
},
}).then((user) => {
if (!user) {
createUser(telegramResponse.id, (success) => {
done({ authorized: success });
});
} else {
done({ authorized: true });
}
});
}
exports.LoginRoute = LoginRoute;

41
src/routes/VerifyRoute.js

@ -0,0 +1,41 @@
const { User } = require("../db/db");
const { addContact } = require("../db/utils");
function VerifyRoute(req, res) {
checkVerification(req.body.id, (success, msg, withUserID) => {
req.session.verified = success;
req.session.verifiedBy = withUserID;
if (success) {
if (req.session.user) {
// If Logged In
addContact(req.session.user, withUserID, (success, msg) => {
res
.status(success ? 200 : 400)
.send({ success: success, message: msg, loggedIn: true });
});
} else {
// If Not Logged In
res.send({ success: success, message: msg, loggedIn: false });
}
} else {
res.status(400).send({ success: success, message: msg });
}
});
}
function checkVerification(id, done) {
User.findOne({
where: {
verification: decodeURIComponent(id),
},
}).then((user) => {
if (user) {
done(true, "User verified", user.id);
} else {
done(false, "No such verification");
}
});
}
exports.VerifyRoute = VerifyRoute;

18
src/session.js

@ -0,0 +1,18 @@
const { store } = require("./db/db");
const sessionOpts = {
secret: process.env.SERVER_SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
sameSite: "none",
maxAge: Number(process.env.SESSION_LENGTH),
},
store: store,
};
const corsOpts = { credentials: true, origin: true, secure: true };
exports.sessionOpts = sessionOpts;
exports.corsOpts = corsOpts;

38
src/telegram.js

@ -0,0 +1,38 @@
const { default: axios } = require("axios");
function setTelegramWebHook(done) {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/setWebhook`;
axios
.post(url, {
url: `${process.env.SERVER_API_URL}/${process.env.TELEGRAM_SECRET}`,
allowed_updates: [],
drop_pending_updates: true,
})
.then((res) => {
done(res);
})
.catch((err) => {
done(err);
});
}
function sendTelegramMessage(telegramID, message, done) {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`;
axios
.post(url, {
chat_id: telegramID,
text: message,
})
.then((res) => {
done(res);
})
.catch((err) => {
console.error("Problem sending Telegram message.");
done(err);
});
}
setTelegramWebHook(() => {});
exports.sendTelegramMessage = sendTelegramMessage;
exports.setTelegramWebHook = setTelegramWebHook;

5
src/utils.js

@ -0,0 +1,5 @@
function getCookieExpiry() {
return new Date(Date.now() + process.env.COOKIE_EXPIRY_DURATION);
}
exports.getCookieExpiry = getCookieExpiry;
Loading…
Cancel
Save