diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 9ae7956..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - env: { - node: true, - commonjs: true, - es2021: true, - }, - extends: "eslint:recommended", - parserOptions: { - ecmaVersion: 12, - }, - rules: {}, -}; diff --git a/.gitignore b/.gitignore index be303de..b6347cf 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,5 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +dist/**/* \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/package-lock.json b/package-lock.json index 4ffc3c8..99d31f5 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 72dbf92..dcf6d2d 100644 --- a/package.json +++ b/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" } diff --git a/src/app.js b/src/app.js deleted file mode 100644 index aacc8fc..0000000 --- a/src/app.js +++ /dev/null @@ -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}`); -}); diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..f0d4dc0 --- /dev/null +++ b/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}`); +}); diff --git a/src/db/db.js b/src/db/db.js deleted file mode 100644 index 88f3469..0000000 --- a/src/db/db.js +++ /dev/null @@ -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; diff --git a/src/db/db.ts b/src/db/db.ts new file mode 100644 index 0000000..180856b --- /dev/null +++ b/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(); diff --git a/src/db/models/Contact.helper.ts b/src/db/models/Contact.helper.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/db/models/Contact.ts b/src/db/models/Contact.ts new file mode 100644 index 0000000..c009cca --- /dev/null +++ b/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 {} +export const Contact = sequelize.define("Contact", { + user: { + type: DataTypes.INTEGER, + allowNull: false, + }, + with: { + type: DataTypes.INTEGER, + allowNull: false, + }, +}); + +Contact.sync(); diff --git a/src/db/models/User.helper.ts b/src/db/models/User.helper.ts new file mode 100644 index 0000000..7e6bb8c --- /dev/null +++ b/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 { + const user = await User.findOne({ + where: { + telegram: telegramID, + }, + }); + return user; +} + +export async function getUserByRowID( + rowID: UserRowID +): Promise { + const user = await User.findOne({ + where: { + id: rowID, + }, + }); + return user; +} + +export async function getUserByVerification( + verification: VerificationString +): Promise { + const user = await User.findOne({ + where: { + verification: verification, + }, + }); + return user; +} + +export async function getUserCovidPositivity( + telegramID: TelegramID +): Promise { + 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 { + 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"); +} diff --git a/src/db/models/User.ts b/src/db/models/User.ts new file mode 100644 index 0000000..36900b5 --- /dev/null +++ b/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 {} + +export const User = sequelize.define("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."); + }); + } +}); diff --git a/src/db/utils.js b/src/db/utils.js deleted file mode 100644 index b396bdc..0000000 --- a/src/db/utils.js +++ /dev/null @@ -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; diff --git a/src/db/utils.ts b/src/db/utils.ts new file mode 100644 index 0000000..3379e82 --- /dev/null +++ b/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 { + 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 { + const user = await User.create({ + telegram: telegram, + }); + return user; +} diff --git a/src/routes/CodeRoute.js b/src/routes/CodeRoute.js deleted file mode 100644 index 78372e9..0000000 --- a/src/routes/CodeRoute.js +++ /dev/null @@ -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; diff --git a/src/routes/CodeRoute.ts b/src/routes/CodeRoute.ts new file mode 100644 index 0000000..c06babb --- /dev/null +++ b/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 { + 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 { + 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; +} diff --git a/src/routes/CovidRoute.ts b/src/routes/CovidRoute.ts new file mode 100644 index 0000000..d2c9584 --- /dev/null +++ b/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" }); + } +} diff --git a/src/routes/LoginRoute.js b/src/routes/LoginRoute.js deleted file mode 100644 index 89d0292..0000000 --- a/src/routes/LoginRoute.js +++ /dev/null @@ -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; diff --git a/src/routes/LoginRoute.ts b/src/routes/LoginRoute.ts new file mode 100644 index 0000000..c940910 --- /dev/null +++ b/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 { + 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)); + } +} diff --git a/src/routes/TelegramWebhookRoute.js b/src/routes/TelegramWebhookRoute.js deleted file mode 100644 index e0c78d6..0000000 --- a/src/routes/TelegramWebhookRoute.js +++ /dev/null @@ -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; diff --git a/src/routes/TelegramWebhookRoute.ts b/src/routes/TelegramWebhookRoute.ts new file mode 100644 index 0000000..3c1c3be --- /dev/null +++ b/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 { + 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 { + const user = await getUserByTelegramID(telegramID); + if (!user) throw new Error("User not found"); + user.isInfected = true; + await user.save(); +} diff --git a/src/routes/VerifyRoute.js b/src/routes/VerifyRoute.js deleted file mode 100644 index 4f39cfb..0000000 --- a/src/routes/VerifyRoute.js +++ /dev/null @@ -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; diff --git a/src/routes/VerifyRoute.ts b/src/routes/VerifyRoute.ts new file mode 100644 index 0000000..0197ab1 --- /dev/null +++ b/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"}); + } +} diff --git a/src/session.js b/src/session.js deleted file mode 100644 index 4812e45..0000000 --- a/src/session.js +++ /dev/null @@ -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; diff --git a/src/session.ts b/src/session.ts new file mode 100644 index 0000000..dd1739c --- /dev/null +++ b/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 }; diff --git a/src/strings.js b/src/strings.ts similarity index 91% rename from src/strings.js rename to src/strings.ts index 210f529..6c608b7 100644 --- a/src/strings.js +++ b/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; diff --git a/src/telegram.js b/src/telegram.js deleted file mode 100644 index 4fb241f..0000000 --- a/src/telegram.js +++ /dev/null @@ -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; diff --git a/src/telegram.ts b/src/telegram.ts new file mode 100644 index 0000000..9cca20a --- /dev/null +++ b/src/telegram.ts @@ -0,0 +1,30 @@ +import axios from "axios"; +import { TelegramID } from "./types"; + +export async function setTelegramWebHook(): Promise { + 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 { + 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); +}); + diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..a2f39b6 --- /dev/null +++ b/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; + } +} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index ea1a9a1..0000000 --- a/src/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -function getCookieExpiry() { - return new Date(Date.now() + process.env.COOKIE_EXPIRY_DURATION); -} - -exports.getCookieExpiry = getCookieExpiry; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..6f3cbe4 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,3 @@ +export function getCookieExpiry(): Date { + return new Date(Date.now() + Number(process.env.COOKIE_EXPIRY_DURATION)); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ce20dfc --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + "include": ["./src/**/*"], + "compilerOptions": { + "outDir": "./dist" + } +}