Browse Source

Merge pull request #7 from naresh97/tsMigration

Migrate to TypeScript
pull/9/head
Nareshkumar Rao 3 years ago
committed by GitHub
parent
commit
ee5888385a
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. 21
      src/app.ts
  7. 89
      src/db/db.js
  8. 44
      src/db/db.ts
  9. 0
      src/db/models/Contact.helper.ts
  10. 23
      src/db/models/Contact.ts
  11. 80
      src/db/models/User.helper.ts
  12. 50
      src/db/models/User.ts
  13. 48
      src/db/utils.js
  14. 56
      src/db/utils.ts
  15. 47
      src/routes/CodeRoute.js
  16. 54
      src/routes/CodeRoute.ts
  17. 61
      src/routes/CovidRoute.js
  18. 26
      src/routes/CovidRoute.ts
  19. 78
      src/routes/LoginRoute.js
  20. 87
      src/routes/LoginRoute.ts
  21. 97
      src/routes/TelegramWebhookRoute.js
  22. 99
      src/routes/TelegramWebhookRoute.ts
  23. 41
      src/routes/VerifyRoute.js
  24. 44
      src/routes/VerifyRoute.ts
  25. 18
      src/session.js
  26. 16
      src/session.ts
  27. 4
      src/strings.ts
  28. 26
      src/telegram.ts
  29. 14
      src/types.ts
  30. 5
      src/utils.js
  31. 3
      src/utils.ts
  32. 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": "tsc && NODE_ENV=production 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"
}

21
src/app.js → src/app.ts

@ -1,14 +1,15 @@
const express = require("express");
const session = require("express-session");
const cors = require("cors");
require("dotenv-flow").config();
import * as dotenvFlow from "dotenv-flow";
dotenvFlow.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");
const { CovidRoute } = require("./routes/CovidRoute");
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}`);

89
src/db/db.js

@ -1,89 +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,
},
infectionDate: {
type: DataTypes.DATE,
}
});
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();

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

@ -0,0 +1,80 @@
import { TelegramID, UserRowID, VerificationString } from "../../types";
import { User, UserInstance } from "./User";
export function getUserByTelegramID(
telegramID: TelegramID,
callback: (user?: UserInstance, message?: string) => void
): void {
User.findOne({
where: {
telegram: telegramID,
},
})
.then((result) => {
callback(!!result ? result : undefined);
})
.catch(() => {
callback(undefined);
});
}
export function getUserByRowID(
rowID: UserRowID,
callback: (user?: UserInstance, message?: string) => void
): void {
User.findOne({
where: {
id: rowID,
},
})
.then((result) => {
callback(!!result ? result : undefined);
})
.catch(() => {
callback(undefined);
});
}
export function getUserByVerification(
verification: VerificationString,
callback: (user?: UserInstance, message?: string) => void
): void {
User.findOne({
where: {
verification: verification,
},
})
.then((result) => {
callback(!!result ? result : undefined);
})
.catch(() => {
callback(undefined);
});
}
export function getUserCovidPositivity(telegramID: TelegramID, callback: (isInfected?: boolean) => void): void {
getUserByTelegramID(telegramID, user => {
if (!!user) {
const infectionDuration = +user.infectionDate - Date.now();
if (infectionDuration > 60 * 60 * 24 * 14) {
setUserCovidPositivity(telegramID, false, success => {
callback(success ? false : undefined);
});
} else {
callback(user.isInfected);
}
} else {
callback();
}
});
}
export function setUserCovidPositivity(telegramID: TelegramID, infectionState: boolean, callback: (success: boolean) => void): void {
getUserByTelegramID(telegramID, user => {
if (!!user) {
user.isInfected = infectionState;
user.infectionDate = new Date();
user.save().then(() => callback(true)).catch(() => callback(false));
} else { callback(false) }
});
}

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;

56
src/db/utils.ts

@ -0,0 +1,56 @@
import { strings_en } from "../strings";
import { sendTelegramMessage } from "../telegram";
import { TelegramID, UserRowID } from "../types";
import { Contact } from "./models/Contact";
import { User } from "./models/User";
import { getUserByRowID, getUserByTelegramID } from "./models/User.helper";
export function addContact(
userATelegram: TelegramID,
userBTelegram: TelegramID,
callback: (success: boolean, message?: string) => void
): void {
getUserByTelegramID(userATelegram, (userA) => {
getUserByTelegramID(userBTelegram, (userB) => {
if (!userA || !userB) {
callback(false, "Could not find user.");
return;
}
Contact.create({ user: userA.id, with: userB.id })
.then(() => {
console.log(
`Registering contact between ${userA.id} and ${userB.id}`
);
sendTelegramMessage(userB.telegram, strings_en.telegram_qr_scanned);
callback(true, "Successfully added contact");
})
.catch((e) => {
callback(false, e);
});
});
});
}
export function createUser(
telegram: TelegramID,
callback: (success: boolean, message: string) => void
): void {
User.create({
telegram: telegram,
})
.then((user) => {
if (!user) {
callback(false, "Could not create user");
} else {
callback(true, "Success");
}
})
.catch((reason) => {
if (reason.name == "SequelizeUniqueConstraintError") {
callback(false, "User already exists");
} else {
callback(false, "Unknown error");
}
});
}

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;

54
src/routes/CodeRoute.ts

@ -0,0 +1,54 @@
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 function CodeRoute(req: Request, res: Response) {
if (!req.session.userTelegramID) {
res.status(401).send("Not logged in");
return;
}
createQRCode(req.session.userTelegramID, (err, url) => {
res.status(url ? 200 : 401).send({ error: err, data: url });
});
}
function createQRCode(
telegram: TelegramID,
callback: (errorMessage: string | Error, url?: string) => void
): void {
getUserByTelegramID(telegram, (user) => {
!!user &&
refreshVerification(user, (result) => {
const verifyURL = `${
process.env.WEBSITE_URL
}/#/verify/${encodeURIComponent(result.verification)}`;
QRCode.toDataURL(
verifyURL,
{ width: 300, height: 300 } as QRCodeToDataURLOptions,
(error, url) => {
callback(error, url);
}
);
});
});
}
function refreshVerification(
user: UserInstance,
callback: (success: UserInstance) => void
): void {
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;
user.save().then((result) => {
callback(result);
});
}

61
src/routes/CovidRoute.js

@ -1,61 +0,0 @@
const { User } = require("../db/db");
function CovidRoute(req, res){
if(!req.session.user){
res.status(401).send("Not logged in");
return;
}
console.log(`SetPositive: ${req.body.setPositive}`);
if(req.body.setPositive){
setUserCovidPositive(req.session.user, true, response=>{
res.send({covidPositive: response});
});
}else{
getUserCovidPositivity(req.session.user, (success, positivity)=>{
res.status(success ? 200 : 400).send({covidPositive: positivity});
});
}
}
function getUserCovidPositivity(telegramID, callback){
User.findOne({
where: {telegram: telegramID},
})
.then(user=>{
if(user){
const infectionDuration = user.infectionDate - Date.now();
if(infectionDuration > 60 * 60 * 24 * 14){
setUserCovidPositive(telegramID, false, res=>{
callback(res, res ? false : null);
});
}else{
callback(true, user.isInfected);
}
}else{
callback(false, null);
}
})
.catch(()=>{
callback(false, null);
})
}
function setUserCovidPositive(telegramID, infectionState, callback){
User.findOne({
where: {telegram: telegramID},
})
.then(user=>{
if(user){
user.isInfected = infectionState;
user.infectionDate = Date.now();
user.save().then(()=>callback(true)).catch(()=>callback(false));
}else{
callback(false);
}
})
.catch(()=>callback(false));
}
exports.CovidRoute = CovidRoute;

26
src/routes/CovidRoute.ts

@ -0,0 +1,26 @@
import { Request, Response } from "express";
import { getUserCovidPositivity, setUserCovidPositivity } from "../db/models/User.helper";
interface CovidRouteRequest extends Request {
body:{
setPositive: boolean;
}
}
export function CovidRoute(req: CovidRouteRequest, res:Response){
if(!req.session.userTelegramID){
res.status(401).send("Not logged in");
return;
}
if(req.body.setPositive){
setUserCovidPositivity(req.session.userTelegramID, true, success=>{
res.send({covidPositive: true});
});
}else{
getUserCovidPositivity(req.session.userTelegramID, isInfected=>{
res.send({covidPositive: isInfected});
});
}
}

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 function LoginRoute(req: LoginRequest, res: Response) {
const telegramResponse = req.body.telegramResponse;
authUser(telegramResponse, (authorized) => {
if (authorized) {
// User is already logged in
if (req.session.userTelegramID == telegramResponse.id) {
res.send({authorized: authorized});
return;
}
const verified = req.session.isVerified;
const verifiedBy = req.session.verifiedByTelegramID;
req.session.regenerate(() => {
req.session.userTelegramID = telegramResponse.id;
if (verified) {
addContact(telegramResponse.id, verifiedBy, (success) => {
res.send({
authorized: authorized,
contactSuccess: success,
});
});
} else {
res.send({authorized: authorized});
}
});
} else {
res.status(401).send(authorized);
}
});
}
function authUser(
telegramResponse: TelegramLoginResponse,
callback: (authorized: boolean, message?: string) => void
): void {
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) {
callback(false);
return;
}
getUserByTelegramID(telegramResponse.id, (user) => {
if (!!user) {
callback(true);
} else {
createUser(telegramResponse.id, (success, message) => {
callback(success, message);
});
}
});
}

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;

99
src/routes/TelegramWebhookRoute.ts

@ -0,0 +1,99 @@
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 function TelegramWebhookRoute(
req: TelegramWebhookRequest,
res: Response
) {
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, (success) => {
if (success) {
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: TelegramID) {
getUserByTelegramID(telegramID, (user) => {
if (user) {
Contact.findAll({
where: {
[Op.or]: [{ user: user.id }, { with: user.id }],
},
}).then((result) => {
result.forEach((contact) => {
const otherPersonID =
contact.user == user.id ? contact.with : contact.user;
getUserByRowID(otherPersonID, (otherUser) => {
otherUser &&
sendTelegramMessage(
otherUser.telegram,
strings_en.telegram_inform_infect
);
});
});
});
}
});
}
function userInfected(
telegramID: TelegramID,
callback: (success: boolean) => void
): void {
getUserByTelegramID(telegramID, (user) => {
if (!!user) {
user.isInfected = true;
user
.save()
.then((result) => {
callback(!!result);
})
.catch(() => {
callback(false);
});
} else {
callback(false);
}
});
}

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;

44
src/routes/VerifyRoute.ts

@ -0,0 +1,44 @@
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 function VerifyRoute(req: VerifyRequest, res: Response) {
getUserByVerification(
decodeURIComponent(req.body.id) as VerificationString,
(verifiedByUser, message) => {
if (!!verifiedByUser) {
req.session.isVerified = !!verifiedByUser;
req.session.verifiedByTelegramID = verifiedByUser.telegram;
if (req.session.userTelegramID) {
// If Logged In
addContact(
req.session.userTelegramID,
verifiedByUser.telegram,
(success, message) => {
res
.status(success ? 200 : 400)
.send({ success: success, message: message, loggedIn: true });
}
);
} else {
// If Not Logged In
res.send({
success: !!verifiedByUser,
message: message,
loggedIn: false,
});
}
} else {
res.status(400).send({ success: !!verifiedByUser, message: message });
}
}
);
}

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;

26
src/telegram.js → src/telegram.ts

@ -1,6 +1,9 @@
const { default: axios } = require("axios");
import axios from "axios";
import { TelegramID } from "./types";
function setTelegramWebHook(done) {
export function setTelegramWebHook(
callback: (success: boolean) => void = () => {}
): void {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/setWebhook`;
axios
.post(url, {
@ -9,14 +12,18 @@ function setTelegramWebHook(done) {
drop_pending_updates: true,
})
.then((res) => {
done(res);
callback(!!res);
})
.catch((err) => {
done(err);
callback(!!err);
});
}
function sendTelegramMessage(telegramID, message, done = () => {}) {
export function sendTelegramMessage(
telegramID: TelegramID,
message: string,
callback: (success: boolean) => void = () => {}
): void {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_TOKEN}/sendMessage`;
axios
.post(url, {
@ -24,15 +31,12 @@ function sendTelegramMessage(telegramID, message, done = () => {}) {
text: message,
})
.then((res) => {
done(res);
callback(!!res);
})
.catch((err) => {
console.error("Problem sending Telegram message.");
done(err);
callback(!!err);
});
}
setTelegramWebHook(() => {});
exports.sendTelegramMessage = sendTelegramMessage;
exports.setTelegramWebHook = setTelegramWebHook;
setTelegramWebHook();

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