Browse Source

Merge pull request #9 from naresh97/development

Release TS Migration
pull/13/head
Nareshkumar Rao 3 years ago
committed by GitHub
parent
commit
4db22a0585
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      .eslintrc.js
  2. 2
      .gitignore
  3. 1
      .vscode/settings.json
  4. 237
      package-lock.json
  5. 10
      package.json
  6. 28
      src/app.js
  7. 31
      src/app.ts
  8. 86
      src/db/db.js
  9. 44
      src/db/db.ts
  10. 0
      src/db/models/Contact.helper.ts
  11. 23
      src/db/models/Contact.ts
  12. 60
      src/db/models/User.helper.ts
  13. 50
      src/db/models/User.ts
  14. 48
      src/db/utils.js
  15. 30
      src/db/utils.ts
  16. 47
      src/routes/CodeRoute.js
  17. 49
      src/routes/CodeRoute.ts
  18. 33
      src/routes/CovidRoute.ts
  19. 78
      src/routes/LoginRoute.js
  20. 87
      src/routes/LoginRoute.ts
  21. 97
      src/routes/TelegramWebhookRoute.js
  22. 78
      src/routes/TelegramWebhookRoute.ts
  23. 41
      src/routes/VerifyRoute.js
  24. 38
      src/routes/VerifyRoute.ts
  25. 18
      src/session.js
  26. 16
      src/session.ts
  27. 4
      src/strings.ts
  28. 38
      src/telegram.js
  29. 30
      src/telegram.ts
  30. 14
      src/types.ts
  31. 5
      src/utils.js
  32. 3
      src/utils.ts
  33. 7
      tsconfig.json

12
.eslintrc.js

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

2
.gitignore

@ -117,3 +117,5 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
dist/**/*

1
.vscode/settings.json

@ -0,0 +1 @@
{}

237
package-lock.json

@ -21,6 +21,13 @@
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.1",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12",
"@types/dotenv-flow": "^3.1.1",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/qrcode": "^1.4.1",
"eslint": "^7.31.0",
"prettier": "2.3.2"
}
@ -218,11 +225,126 @@
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"node_modules/@types/bcrypt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
"integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==",
"dev": true,
"dependencies": {
"@types/connect": "*",
"@types/node": "*"
}
},
"node_modules/@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"node_modules/@types/dotenv-flow": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.1.1.tgz",
"integrity": "sha512-khxgP+KkHPL72SP0Wqn1gB6EHj6yk79OBGJEKW64XL13RbyDGTkRbbA47VICOLRrvgKOpZeun2uMsgO7pAsExQ==",
"dev": true
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
"integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
"dev": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.18",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/express-serve-static-core": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz",
"integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==",
"dev": true,
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"node_modules/@types/express-session": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
"dev": true,
"dependencies": {
"@types/express": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"node_modules/@types/node": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.1.tgz",
"integrity": "sha512-UW7cbLqf/Wu5XH2RKKY1cHwUNLicIDRLMraYKz+HHAerJ0ZffUEk+fMnd8qU2JaS6cAy0r8tsaf7yqHASf/Y0Q=="
},
"node_modules/@types/qrcode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.1.tgz",
"integrity": "sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/serve-static": {
"version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
"integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
"dev": true,
"dependencies": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -4600,11 +4722,126 @@
"tar": "^6.1.0"
}
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==",
"dev": true
},
"@types/bcrypt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.0.tgz",
"integrity": "sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/body-parser": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz",
"integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.35",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"@types/dotenv-flow": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/dotenv-flow/-/dotenv-flow-3.1.1.tgz",
"integrity": "sha512-khxgP+KkHPL72SP0Wqn1gB6EHj6yk79OBGJEKW64XL13RbyDGTkRbbA47VICOLRrvgKOpZeun2uMsgO7pAsExQ==",
"dev": true
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
"integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^4.17.18",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.24",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz",
"integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/express-session": {
"version": "1.17.4",
"resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.17.4.tgz",
"integrity": "sha512-7cNlSI8+oOBUHTfPXMwDxF/Lchx5aJ3ho7+p9jJZYVg9dVDJFh3qdMXmJtRsysnvS+C6x46k9DRYmrmCkE+MVg==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
"dev": true
},
"@types/node": {
"version": "16.4.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.4.1.tgz",
"integrity": "sha512-UW7cbLqf/Wu5XH2RKKY1cHwUNLicIDRLMraYKz+HHAerJ0ZffUEk+fMnd8qU2JaS6cAy0r8tsaf7yqHASf/Y0Q=="
},
"@types/qrcode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.4.1.tgz",
"integrity": "sha512-vxMyr7JM7tYPxu8vUE83NiosWX5DZieCyYeJRoOIg0pAkyofCBzknJ2ycUZkPGDFis2RS8GN/BeJLnRnAPxeCA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.10",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz",
"integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==",
"dev": true,
"requires": {
"@types/mime": "^1",
"@types/node": "*"
}
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",

10
package.json

@ -5,7 +5,8 @@
"main": "src/app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/app.js"
"start": "node dist/app.js",
"dev": "(NODE_ENV=development; tsc && node dist/app.js)"
},
"author": "",
"license": "ISC",
@ -23,6 +24,13 @@
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@tsconfig/node14": "^1.0.1",
"@types/bcrypt": "^5.0.0",
"@types/cors": "^2.8.12",
"@types/dotenv-flow": "^3.1.1",
"@types/express": "^4.17.13",
"@types/express-session": "^1.17.4",
"@types/qrcode": "^1.4.1",
"eslint": "^7.31.0",
"prettier": "2.3.2"
}

28
src/app.js

@ -1,28 +0,0 @@
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");
const { TelegramWebhookRoute } = require("./routes/TelegramWebhookRoute");
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}`, TelegramWebhookRoute);
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}`);
});

31
src/app.ts

@ -0,0 +1,31 @@
import * as dotenvFlow from "dotenv-flow";
dotenvFlow.config();
import express from "express";
import session from "express-session";
import cors from "cors";
import { corsOpts, sessionOpts } from "./session";
import { TelegramWebhookRoute } from "./routes/TelegramWebhookRoute";
import { LoginRoute } from "./routes/LoginRoute";
import { CodeRoute } from "./routes/CodeRoute";
import { VerifyRoute } from "./routes/VerifyRoute";
import { CovidRoute } from "./routes/CovidRoute";
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}`, TelegramWebhookRoute);
app.post("/login", LoginRoute);
app.get("/code", CodeRoute);
app.post("/verify", VerifyRoute);
app.post("/covid", CovidRoute);
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});

86
src/db/db.js

@ -1,86 +0,0 @@
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,
},
isInfected: {
type: DataTypes.BOOLEAN,
},
});
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;

44
src/db/db.ts

@ -0,0 +1,44 @@
import ConnectSessionSequelize from "connect-session-sequelize";
import session from "express-session";
import { Sequelize } from "sequelize";
const SequelizeStore = ConnectSessionSequelize(session.Store);
const isProduction: boolean = process.env.NODE_ENV == "production";
export const sequelize: Sequelize = (() => {
if (isProduction) {
return new Sequelize(
process.env.DB_DATA_NAME || "DATABASE",
process.env.DB_USER || "USERNAME",
process.env.DB_PASS || "PASSWORD",
{
host: process.env.DB_PATH || "localhost",
dialect: "postgres",
}
);
} else {
return new Sequelize("sqlite::memory:");
}
})();
export const storeDB: Sequelize = (() => {
if (isProduction) {
return new Sequelize(
process.env.DB_STORE_NAME || "DATABASE",
process.env.DB_USER || "USERNAME",
process.env.DB_PASS || "PASSWORD",
{
host: process.env.DB_PATH,
dialect: "postgres",
}
);
} else {
return new Sequelize("sqlite::memory:");
}
})();
export const store = new SequelizeStore({
db: storeDB,
});
store.sync();

0
src/db/models/Contact.helper.ts

23
src/db/models/Contact.ts

@ -0,0 +1,23 @@
import { DataTypes, Model } from "sequelize";
import { UserRowID } from "../../types";
import { sequelize } from "../db";
interface ContactAttributes {
user: UserRowID;
with: UserRowID;
}
export interface ContactInterface
extends Model<ContactAttributes, ContactAttributes>,
ContactAttributes {}
export const Contact = sequelize.define<ContactInterface>("Contact", {
user: {
type: DataTypes.INTEGER,
allowNull: false,
},
with: {
type: DataTypes.INTEGER,
allowNull: false,
},
});
Contact.sync();

60
src/db/models/User.helper.ts

@ -0,0 +1,60 @@
import { TelegramID, UserRowID, VerificationString } from "../../types";
import { User, UserInstance } from "./User";
export async function getUserByTelegramID(
telegramID: TelegramID
): Promise<UserInstance | null> {
const user = await User.findOne({
where: {
telegram: telegramID,
},
});
return user;
}
export async function getUserByRowID(
rowID: UserRowID
): Promise<UserInstance | null> {
const user = await User.findOne({
where: {
id: rowID,
},
});
return user;
}
export async function getUserByVerification(
verification: VerificationString
): Promise<UserInstance | null> {
const user = await User.findOne({
where: {
verification: verification,
},
});
return user;
}
export async function getUserCovidPositivity(
telegramID: TelegramID
): Promise<boolean> {
const user = await getUserByTelegramID(telegramID);
if (!user) throw new Error("User not found");
const infectionDuration = new Date().getTime() - user.infectionDate.getTime();
if (infectionDuration > 60 * 60 * 24 * 14 * 1000) {
await setUserCovidPositivity(telegramID, false);
return false;
} else {
return user.isInfected;
}
}
export async function setUserCovidPositivity(
telegramID: TelegramID,
infectionState: boolean
): Promise<void> {
const user = await getUserByTelegramID(telegramID);
if (!user) throw new Error("User not found");
user.isInfected = infectionState;
user.infectionDate = new Date();
if (!(await user.save())) throw new Error("Could not save user state");
}

50
src/db/models/User.ts

@ -0,0 +1,50 @@
import { DataTypes, Model } from "sequelize";
import { TelegramID, UserRowID, VerificationString } from "../../types";
import { sequelize } from "../db";
interface UserAttributes {
id: UserRowID;
telegram: TelegramID;
verification: VerificationString;
isInfected: boolean;
infectionDate: Date;
}
interface UserCreationAttributes {
telegram: TelegramID;
}
export interface UserInstance
extends Model<UserAttributes, UserCreationAttributes>,
UserAttributes {}
export const User = sequelize.define<UserInstance>("User", {
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
telegram: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true,
},
verification: {
type: DataTypes.STRING,
},
isInfected: {
type: DataTypes.BOOLEAN,
defaultValue: false,
},
infectionDate: {
type: DataTypes.DATE,
},
});
User.sync().then(() => {
if (process.env.ADMIN_USERNAME && process.env.ADMIN_PASSWORD) {
User.create({
telegram: 12345 as TelegramID,
}).catch(() => {
console.log("Couldn't create admin account. Probably exists.");
});
}
});

48
src/db/utils.js

@ -1,48 +0,0 @@
const { strings_en } = require("../strings");
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,
strings_en.telegram_qr_scanned,
() => {}
);
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;

30
src/db/utils.ts

@ -0,0 +1,30 @@
import { strings_en } from "../strings";
import { sendTelegramMessage } from "../telegram";
import { TelegramID } from "../types";
import { Contact } from "./models/Contact";
import { User, UserInstance } from "./models/User";
import { getUserByTelegramID } from "./models/User.helper";
export async function addContact(
userATelegram: TelegramID,
userBTelegram: TelegramID
): Promise<void> {
const userA = await getUserByTelegramID(userATelegram);
const userB = await getUserByTelegramID(userBTelegram);
if (!userA || !userB) {
throw new Error("Could not found users");
}
await Contact.create({ user: userA.id, with: userB.id });
await sendTelegramMessage(userB.telegram, strings_en.telegram_qr_scanned);
}
export async function createUser(
telegram: TelegramID
): Promise<UserInstance | null> {
const user = await User.create({
telegram: telegram,
});
return user;
}

47
src/routes/CodeRoute.js

@ -1,47 +0,0 @@
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;

49
src/routes/CodeRoute.ts

@ -0,0 +1,49 @@
import { Request, Response } from "express";
import bcrypt from "bcrypt";
import QRCode, { QRCodeToDataURLOptions } from "qrcode";
import { TelegramID, VerificationString } from "../types";
import { User, UserInstance } from "../db/models/User";
import { getUserByTelegramID } from "../db/models/User.helper";
export async function CodeRoute(req: Request, res: Response) {
if (!req.session.userTelegramID) {
res.status(401).send("Not logged in");
return;
}
try {
const url = await createQRCode(req.session.userTelegramID);
res.send({ data: url });
} catch (error) {
res
.status(500)
.send({ error: error instanceof Error ? error.message : "Error" });
}
}
async function createQRCode(telegram: TelegramID): Promise<string> {
const user = await getUserByTelegramID(telegram);
if (!user) throw new Error("User not found");
const newVerification = await refreshVerification(user);
const verifyURL = `${process.env.WEBSITE_URL}/#/verify/${encodeURIComponent(
newVerification
)}`;
return await QRCode.toDataURL(verifyURL, {
width: 300,
height: 300,
} as QRCodeToDataURLOptions);
}
async function refreshVerification(
user: UserInstance
): Promise<VerificationString> {
let newVerification = bcrypt
.hashSync(`${new Date().getTime()}-${user.telegram}`, 5)
.replace(/[^a-zA-Z0-9]+/g, "") as VerificationString;
newVerification = newVerification.substr(
0,
newVerification.length / 2
) as VerificationString;
user.verification = newVerification;
await user.save();
return newVerification;
}

33
src/routes/CovidRoute.ts

@ -0,0 +1,33 @@
import { Request, Response } from "express";
import {
getUserCovidPositivity,
setUserCovidPositivity,
} from "../db/models/User.helper";
interface CovidRouteRequest extends Request {
body: {
setPositive: boolean;
};
}
export async function CovidRoute(req: CovidRouteRequest, res: Response) {
if (!req.session.userTelegramID) {
res.status(401).send("Not logged in");
return;
}
try {
if (req.body.setPositive) {
await setUserCovidPositivity(req.session.userTelegramID, true);
res.send({ covidPositive: true });
} else {
const isInfected = await getUserCovidPositivity(
req.session.userTelegramID
);
res.send({ covidPositive: isInfected });
}
} catch (error) {
res
.send(500)
.send({ error: error instanceof Error ? error.message : "Error" });
}
}

78
src/routes/LoginRoute.js

@ -1,78 +0,0 @@
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) {
// User is already logged in
if (req.session.user == telegramResponse.id) {
res.send({ authorized: success });
return;
}
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;

87
src/routes/LoginRoute.ts

@ -0,0 +1,87 @@
import { Request, Response } from "express";
import crypto from "crypto";
import { addContact, createUser } from "../db/utils";
import { TelegramID, UserRowID } from "../types";
import { User } from "../db/models/User";
import { getUserByTelegramID } from "../db/models/User.helper";
type TelegramLoginResponse = {
id: TelegramID;
hash: string;
};
interface LoginRequest extends Request {
body: {
telegramResponse: TelegramLoginResponse;
};
}
export async function LoginRoute(req: LoginRequest, res: Response) {
const telegramResponse = req.body.telegramResponse;
try {
const authorized = await authUser(telegramResponse);
if (authorized) {
// User is already logged in
if (req.session.userTelegramID == telegramResponse.id) {
res.send({ authorized: authorized });
return;
}
// User not logged in
const verified = req.session.isVerified;
const verifiedBy = req.session.verifiedByTelegramID;
req.session.regenerate(async () => {
req.session.userTelegramID = telegramResponse.id;
if (verified) {
await addContact(telegramResponse.id, verifiedBy);
res.send({
authorized: true,
contactSuccess: true,
});
} else {
res.send({ authorized: authorized });
}
});
} else {
res.status(401).send({ error: "Unauthorized" });
}
} catch (error) {
res
.status(500)
.send({ error: error instanceof Error ? error.message : "Error" });
}
}
async function authUser(
telegramResponse: TelegramLoginResponse
): Promise<boolean> {
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) {
return false;
}
const user = await getUserByTelegramID(telegramResponse.id);
if (!!user) {
return true;
} else {
return !!(await createUser(telegramResponse.id));
}
}

97
src/routes/TelegramWebhookRoute.js

@ -1,97 +0,0 @@
const { Op } = require("sequelize");
const { User, Contact } = require("../db/db");
const { strings_en } = require("../strings");
const { sendTelegramMessage } = require("../telegram");
function TelegramWebhookRoute(req, res) {
try {
if (req.body.message.connected_website) {
sendTelegramMessage(
req.body.message.from.id,
"Thanks for using OurSejahtera! Let's stay safer together <3"
);
} else {
const messageText = req.body.message.text;
const telegramID = req.body.message.from.id;
if (messageText.toLowerCase() == "/covidpositive") {
userInfected(telegramID, (result) => {
if (result.saved) {
sendTelegramMessage(
telegramID,
strings_en.telegram_inform_positive
);
informContacts(telegramID);
} else {
sendTelegramMessage(telegramID, "Sorry, something went wrong.");
}
});
}
}
} catch (e) {
console.log("Could not get Telegram Message");
}
res.send();
}
function informContacts(telegramID, doneCallback = () => {}) {
User.findOne({
where: {
telegram: telegramID,
},
}).then((user) => {
if (user) {
const userRowID = user.id;
Contact.findAll({
where: {
[Op.or]: [{ user: userRowID }, { with: userRowID }],
},
}).then((result) => {
result.forEach((contact) => {
const otherPersonID =
contact.user == userRowID ? contact.with : contact.user;
User.findOne({
where: {
id: otherPersonID,
},
}).then((otherPerson) => {
sendTelegramMessage(
otherPerson.telegram,
strings_en.telegram_inform_infect
);
});
});
});
}
});
}
function userInfected(telegramID, doneCallback) {
User.findOne({
where: {
telegram: telegramID,
},
})
.then((user) => {
if (!user) {
done({ saved: false });
} else {
user.isInfected = true;
user
.save()
.then((result) => {
if (result) {
doneCallback({ saved: true });
}
})
.catch((err) => {
doneCallback({ saved: false });
});
}
})
.catch((err) => {
doneCallback({ saved: false });
});
}
exports.TelegramWebhookRoute = TelegramWebhookRoute;

78
src/routes/TelegramWebhookRoute.ts

@ -0,0 +1,78 @@
import { Request, Response } from "express";
import { Op } from "sequelize";
import { Contact } from "../db/models/Contact";
import { User } from "../db/models/User";
import { getUserByRowID, getUserByTelegramID } from "../db/models/User.helper";
import { strings_en } from "../strings";
import { sendTelegramMessage } from "../telegram";
import { TelegramID } from "../types";
interface TelegramWebhookRequest extends Request {
body: {
message: {
text: string;
from: {
id: TelegramID;
};
connected_website: string;
};
};
}
export async function TelegramWebhookRoute(
req: TelegramWebhookRequest,
res: Response
) {
try {
if (req.body.message.connected_website) {
await sendTelegramMessage(
req.body.message.from.id,
"Thanks for using OurSejahtera! Let's stay safer together <3"
);
} else {
const messageText = req.body.message.text;
const telegramID = req.body.message.from.id;
if (messageText.toLowerCase() == "/covidpositive") {
await userInfected(telegramID);
await sendTelegramMessage(
telegramID,
strings_en.telegram_inform_positive
);
await informContacts(telegramID);
}
}
} catch (e) {
console.log(
e instanceof Error ? e.message : "Could not get Telegram Message"
);
}
res.send();
}
async function informContacts(telegramID: TelegramID): Promise<void> {
const user = await getUserByTelegramID(telegramID);
if (!user) throw new Error("User not found");
const contacts = await Contact.findAll({
where: {
[Op.or]: [{ user: user.id }, { with: user.id }],
},
});
contacts.forEach(async (contact) => {
const otherPersonID = contact.user == user.id ? contact.with : contact.user;
const otherUser = await getUserByRowID(otherPersonID);
if (!otherUser) throw new Error("Other user does not exist");
await sendTelegramMessage(
otherUser.telegram,
strings_en.telegram_inform_infect
);
});
}
async function userInfected(telegramID: TelegramID): Promise<void> {
const user = await getUserByTelegramID(telegramID);
if (!user) throw new Error("User not found");
user.isInfected = true;
await user.save();
}

41
src/routes/VerifyRoute.js

@ -1,41 +0,0 @@
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;

38
src/routes/VerifyRoute.ts

@ -0,0 +1,38 @@
import { Request, Response } from "express";
import { User } from "../db/models/User";
import { getUserByVerification } from "../db/models/User.helper";
import { addContact } from "../db/utils";
import { UserRowID, VerificationString } from "../types";
interface VerifyRequest extends Request {
body: {
id: VerificationString;
};
}
export async function VerifyRoute(req: VerifyRequest, res: Response) {
const verifiedByUser = await getUserByVerification(
decodeURIComponent(req.body.id) as VerificationString
);
try{
if (!!verifiedByUser) {
req.session.isVerified = !!verifiedByUser;
req.session.verifiedByTelegramID = verifiedByUser.telegram;
if (req.session.userTelegramID) {
// If Logged In
await addContact(req.session.userTelegramID, verifiedByUser.telegram);
res.send({ success: true, loggedIn: true });
} else {
// If Not Logged In
res.send({
success: false,
loggedIn: false,
});
}
} else {
res.status(400).send({ success: false });
}
}catch(e){
res.status(500).send({error: e instanceof Error ? e.message : "Error"});
}
}

18
src/session.js

@ -1,18 +0,0 @@
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;

16
src/session.ts

@ -0,0 +1,16 @@
import { SessionOptions } from "express-session";
import { store } from "./db/db";
export const sessionOpts: SessionOptions = {
secret: process.env.SERVER_SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
sameSite: "none",
maxAge: Number(process.env.SESSION_LENGTH),
},
store: store,
};
export const corsOpts = { credentials: true, origin: true, secure: true };

4
src/strings.js → src/strings.ts

@ -1,4 +1,4 @@
const strings_en = {
export const strings_en = {
telegram_inform_infect:
"ATTENTION! Someone you have been \
in contact with has reported being tested POSITIVE with \
@ -15,5 +15,3 @@ local COVID19 guidelines.",
be notified if they report being tested positive with COVID19. If \
you are tested positive, please tell me /COVIDPOSITIVE",
};
exports.strings_en = strings_en;

38
src/telegram.js

@ -1,38 +0,0 @@
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;

30
src/telegram.ts

@ -0,0 +1,30 @@
import axios from "axios";
import { TelegramID } from "./types";
export async function setTelegramWebHook(): Promise<void> {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/setWebhook`;
await axios.post(url, {
url: `${process.env.SERVER_API_URL}/${process.env.TELEGRAM_SECRET}`,
allowed_updates: [],
drop_pending_updates: true,
});
}
export async function sendTelegramMessage(
telegramID: TelegramID,
message: string
): Promise<void> {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`;
const response = await axios.post(url, {
chat_id: telegramID,
text: message,
});
}
setTelegramWebHook()
.catch(error=>{
console.error("Error setting Telegram Webhook");
error instanceof Error && console.error(error.message);
});

14
src/types.ts

@ -0,0 +1,14 @@
/*
* Branding allows to use nominal typing and avoid errors
*/
export type UserRowID = number & { __userRowIDBrand: any };
export type TelegramID = number & { __telegramIDBrand: any };
export type VerificationString = string & { __verificationStringBrand: any };
declare module "express-session" {
interface Session {
isVerified: boolean;
verifiedByTelegramID: TelegramID;
userTelegramID: TelegramID;
}
}

5
src/utils.js

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

3
src/utils.ts

@ -0,0 +1,3 @@
export function getCookieExpiry(): Date {
return new Date(Date.now() + Number(process.env.COOKIE_EXPIRY_DURATION));
}

7
tsconfig.json

@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node14/tsconfig.json",
"include": ["./src/**/*"],
"compilerOptions": {
"outDir": "./dist"
}
}
Loading…
Cancel
Save