From 81686745163dbbf65cbdef0a58735513d05b5a4f Mon Sep 17 00:00:00 2001 From: Mondei1 Date: Fri, 25 Dec 2020 16:42:23 +0100 Subject: [PATCH] User endpoints created Invoices can now be fetched --- package-lock.json | 170 ++++++++++++++++++++++++++++++++- package.json | 4 +- src/app.ts | 2 + src/controllers/invoice.ts | 41 ++++++++ src/controllers/user.ts | 142 +++++++++++++++++++++++++++ src/helper/invoiceScheduler.ts | 4 +- src/helper/request.ts | 6 ++ src/routes/invoice.ts | 8 +- src/routes/user.ts | 10 ++ 9 files changed, 378 insertions(+), 9 deletions(-) create mode 100644 src/controllers/user.ts create mode 100644 src/helper/request.ts create mode 100644 src/routes/user.ts diff --git a/package-lock.json b/package-lock.json index 7e9644b..c081a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,11 @@ "@types/node": "*" } }, + "@types/component-emitter": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", + "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" + }, "@types/connect": { "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", @@ -54,11 +59,15 @@ "@types/node": "*" } }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" + }, "@types/cors": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz", - "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==", - "dev": true + "integrity": "sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg==" }, "@types/dotenv": { "version": "8.2.0", @@ -69,6 +78,15 @@ "dotenv": "*" } }, + "@types/engine.io": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/engine.io/-/engine.io-3.1.4.tgz", + "integrity": "sha512-98rXVukLD6/ozrQ2O80NAlWDGA4INg+tqsEReWJldqyi2fulC9V7Use/n28SWgROXKm6003ycWV4gZHoF8GA6w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.9", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz", @@ -164,6 +182,26 @@ "@types/node": "*" } }, + "@types/socket.io": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@types/socket.io/-/socket.io-2.1.12.tgz", + "integrity": "sha512-oStc5VFkpb0AsjOxQUj9ztX5Iziatyla/rjZTYbFGoVrrKwd+JU2mtxk7iSl5RGYx9WunLo6UXW1fBzQok/ZyA==", + "dev": true, + "requires": { + "@types/engine.io": "*", + "@types/node": "*", + "@types/socket.io-parser": "*" + } + }, + "@types/socket.io-parser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/socket.io-parser/-/socket.io-parser-2.2.1.tgz", + "integrity": "sha512-+JNb+7N7tSINyXPxAJb62+NcpC1x/fPn7z818W4xeNCdPTp6VsO/X8fCsg6+ug4a56m1v9sEiTIIUKVupcHOFQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/typescript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/typescript/-/typescript-2.0.0.tgz", @@ -283,6 +321,16 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, "bignumber.js": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", @@ -439,6 +487,11 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -562,6 +615,48 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, + "engine.io": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.0.5.tgz", + "integrity": "sha512-Ri+whTNr2PKklxQkfbGjwEo+kCBUM4Qxk4wtLqLrhH+b1up2NFL9g9pjYWiCV/oazwB0rArnvF/ZmZN2ab5Hpg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.1.0", + "engine.io-parser": "~4.0.0", + "ws": "^7.1.2" + }, + "dependencies": { + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "engine.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", + "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", + "requires": { + "base64-arraybuffer": "0.1.4" + } + }, "es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", @@ -1508,6 +1603,72 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "socket.io": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.0.4.tgz", + "integrity": "sha512-Vj1jUoO75WGc9txWd311ZJJqS9Dr8QtNJJ7gk2r7dcM/yGe9sit7qOijQl3GAwhpBOz/W8CwkD7R6yob07nLbA==", + "requires": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.8", + "@types/node": "^14.14.7", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.1.0", + "engine.io": "~4.0.0", + "socket.io-adapter": "~2.0.3", + "socket.io-parser": "~4.0.1" + }, + "dependencies": { + "@types/node": { + "version": "14.14.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", + "integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "socket.io-adapter": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.0.3.tgz", + "integrity": "sha512-2wo4EXgxOGSFueqvHAdnmi5JLZzWqMArjuP4nqC26AtLh5PoCPsaRbRdah2xhcwTAMooZfjYiNVNkkmmSMaxOQ==" + }, + "socket.io-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.2.tgz", + "integrity": "sha512-Bs3IYHDivwf+bAAuW/8xwJgIiBNtlvnjYRc4PbXgniLmcP1BrakBoq/QhO24rgtgW7VZ7uAaswRGxutUnlAK7g==", + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.1.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1753,6 +1914,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "ws": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.1.tgz", + "integrity": "sha512-pTsP8UAfhy3sk1lSk/O/s4tjD0CRwvMnzvwr4OKGX7ZvqZtUyx4KIJB5JWbkykPoc55tixMGgTNoh3k4FkNGFQ==" + }, "yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index bcae04a..f4d1c17 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "jsonwebtoken": "^8.5.1", "mongoose": "^5.11.8", "mysql": "^2.18.1", + "socket.io": "^3.0.4", "ts-node": "^9.1.1", "typescript": "^4.1.3", "winston": "^3.3.3", @@ -46,6 +47,7 @@ "@types/mysql": "2.15.16", "@types/mongoose": "5.10.3", "@types/argon2": "0.15.0", - "@types/zeromq": "4.6.3" + "@types/zeromq": "4.6.3", + "@types/socket.io": "2.1.12" } } diff --git a/src/app.ts b/src/app.ts index 86186de..cec88fa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,6 +11,7 @@ import { hashPassword, randomPepper, randomString } from './helper/crypto'; import { InvoiceScheduler } from './helper/invoiceScheduler'; import { User } from './models/user/user.model'; import { invoiceRouter } from './routes/invoice'; +import { userRouter } from './routes/user'; // Load .env dconfig({ debug: true, encoding: 'UTF-8' }); @@ -110,6 +111,7 @@ async function run() { app.get('/', (req, res) => res.status(200).send('OK')); app.use('/invoice', invoiceRouter); + app.use('/user', userRouter); app.listen(config.http.port, config.http.host, () => { logger.info(`HTTP server started on port ${config.http.host}:${config.http.port}`); diff --git a/src/controllers/invoice.ts b/src/controllers/invoice.ts index c57ec7c..75e0495 100644 --- a/src/controllers/invoice.ts +++ b/src/controllers/invoice.ts @@ -76,4 +76,45 @@ export async function createInvoice(req: Request, res: Response) { }); }); +} + +// GET /invoice/ +// GET /invoice/:id +export async function getInvoice(req: Request, res: Response) { + const invoiceId = req.params.id; + + // If an id is provided + if (invoiceId !== undefined) { + const invoice: any = await Invoice.findById(invoiceId); + if (invoice === null) { + res.status(404).send(); + return; + } + + res.status(200).send(invoice); + return; + } + + let skip = req.query.skip; + let limit = req.query.limit; + let sortQuery = req.query.sort; // Either 'newest' (DESC) or 'oldest' (ASC) + let sort = 1; + + if (skip === undefined) skip = '0'; + if (limit === undefined || Number(limit) > 100) limit = '100'; + if (sortQuery !== undefined) { + if (sortQuery === 'newest') sort = -1; + else if (sortQuery === 'newest') sort = 1; + else { + res.status(400).send({ message: 'Unkown sort parameter. "sort" can only be "newest" or "oldest"' }); + return; + } + } + + const invoices = await Invoice.find({}) + .limit(Number(limit)) + .skip(Number(skip)) + .sort({ createdAt: sort }); + + res.status(200).send(invoices); } \ No newline at end of file diff --git a/src/controllers/user.ts b/src/controllers/user.ts new file mode 100644 index 0000000..063abd1 --- /dev/null +++ b/src/controllers/user.ts @@ -0,0 +1,142 @@ +import { Request, Response } from 'express'; +import { decode, sign, verify } from 'jsonwebtoken'; + +import { JWT_SECRET, logger } from '../app'; +import * as jwt from 'jsonwebtoken'; +import { config } from '../../config'; +import { hashPassword, randomPepper, randomString, verifyPassword } from '../helper/crypto'; +import { User } from '../models/user/user.model'; +import { LibrePayRequest } from '../helper/request'; + +export async function getUser(req: LibrePayRequest, res: Response) { + let user: any = req.params.id === undefined ? req.user : await User.findById(req.params.id); + + if (user === null) { + res.status(404).send(); + return; + } + + user.password = undefined; + user.salt = undefined; + user.__v = undefined; + + res.status(200).send(user); +} + +export async function createUser(req: LibrePayRequest, res: Response) { + const name = req.body.name; + const password = req.body.password; + const type = req.body.type; + + if (name === undefined || password === undefined || type === undefined) { + res.status(400).send(); + return; + } + + if (await User.countDocuments({ name }) === 1) { + res.status(409).send(); + return; + } + + const salt = randomString(config.authentification.salt_length); + const hashedPassword = await hashPassword(password + salt + randomPepper()).catch(error => { + res.status(400).send({ message: 'Provided password is too weak and cannot be used.' }); + return; + }) as string; + + const newUser = await User.create({ + name, + password: hashedPassword, + salt, + lastLogin: new Date(0) + }); + + // Create setup token that the new user can use to change his password. + const setupToken = jwt.sign({ setupForUser: newUser._id }, JWT_SECRET, { expiresIn: '1d' }); + + res.status(200).send({ setupToken }); +} + +export async function DeleteUser(req: Request, res: Response) { + +} + +export async function PatchUser(req: Request, res: Response) { + +} + +export async function loginUser(req: Request, res: Response) { + const username = req.body.username; + const password = req.body.password; + const twoFA = req.body.twoFA; + + const user = await User.findOne({ name: username }); + + // Check if user exists + if (user == undefined) { + setTimeout(() => { + res.status(404).send({ message: "Either the username or password is wrong." }); + }, Math.random() * 1500 + 400); + return; + } + + // Check if 2FA is turned on (the attack doesn't know yet if the password is wrong) + if (user.twoFASecret != undefined) { + if (twoFA == undefined) { + res.status(401).send({ message: "2FA code is required." }); + return; + } + // TODO: Implement 2FA logic here + } + + // Check if password is wrong + if (!await verifyPassword(password + user.salt, user.password)) { + res.status(404).send({ message: 'Either the username or password is wrong.' }); + return; + } + + // We're good. Create JWT token. + const token = sign({ user: user._id }, JWT_SECRET, { expiresIn: '30d' }); + + user.lastLogin = new Date(Date.now()); + await user.save(); + + logger.info(`User ${user.name} logged in.`) + res.status(200).send({ token }); +} + +/** + * This middleware validates any tokens that are required to access most of the endpoints. + * Note: This validation doesn't contain any permission checking. + */ +export async function MW_User(req: LibrePayRequest, res: Response, next: () => void) { + if (req.headers.token === undefined) { + res.status(401).send({ message: "Token not specified" }); + return; + } + const token = req.headers.token.toString(); + + try { + // Verify token + if(await verify(token, JWT_SECRET, { algorithms: ['HS256'] })) { + // Token is valid, now look if user is in db (in case he got deleted) + const id = decode(token, { json: true })!.user; + const db = await User.findById(id); + + if (db !== undefined && db !== null) { + req.user = db + next(); + return; + } else { + res.status(401).send({ message: "Token is not valid" }); + } + } else { + res.status(401).send({ message: "Token is not valid" }); + } + } catch (err) { + if (err) { + res.status(500).send({ message: "We failed validating your token for some reason." }); + logger.error(err); + } + } +} \ No newline at end of file diff --git a/src/helper/invoiceScheduler.ts b/src/helper/invoiceScheduler.ts index 9fa737c..3a1a1e1 100644 --- a/src/helper/invoiceScheduler.ts +++ b/src/helper/invoiceScheduler.ts @@ -51,9 +51,7 @@ export class InvoiceScheduler { return; } - decoded.result.vout.forEach(output => { - //console.log('Output:', output.scriptPubKey); - + decoded.result.vout.forEach(output => { // Loop over each output and check if the address of one matches the one of an invoice. this.pendingInvoices.forEach(invoice => { // We found our transaction diff --git a/src/helper/request.ts b/src/helper/request.ts new file mode 100644 index 0000000..ac32987 --- /dev/null +++ b/src/helper/request.ts @@ -0,0 +1,6 @@ +import { Request } from "express"; +import { IUser } from "../models/user/user.interface"; + +export interface LibrePayRequest extends Request { + user?: IUser +} \ No newline at end of file diff --git a/src/routes/invoice.ts b/src/routes/invoice.ts index 9237bec..768e09f 100644 --- a/src/routes/invoice.ts +++ b/src/routes/invoice.ts @@ -1,9 +1,11 @@ import { Router } from "express"; -import { createInvoice } from "../controllers/invoice"; +import { createInvoice, getInvoice } from "../controllers/invoice"; +import { MW_User } from "../controllers/user"; const invoiceRouter = Router() -invoiceRouter.get('/:id'); -invoiceRouter.post('/', createInvoice); +invoiceRouter.get('/:id', getInvoice); +invoiceRouter.get('/', MW_User, getInvoice); +invoiceRouter.post('/', MW_User, createInvoice); export { invoiceRouter }; \ No newline at end of file diff --git a/src/routes/user.ts b/src/routes/user.ts new file mode 100644 index 0000000..32b0c4c --- /dev/null +++ b/src/routes/user.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { MW_User, loginUser, getUser } from "../controllers/user"; + +const userRouter = Router() + +userRouter.get('/login', loginUser); +userRouter.get('/', MW_User, getUser); +userRouter.get('/:id', MW_User, getUser); + +export { userRouter }; \ No newline at end of file