From f743521fae40eb7706f99a5c1e3b1255895833c3 Mon Sep 17 00:00:00 2001 From: CookieDasora Date: Wed, 12 Jul 2023 15:05:17 -0300 Subject: [PATCH] Implemented rate limit, added Redis database, added more verifications --- .env.example | 7 +- README.md | 6 +- docker-compose.yml | 15 ++- package-lock.json | 174 ++++++++++++++++++++++++++++++ package.json | 3 + src/controllers/users-router.ts | 3 +- src/middlewares/rate-limit.ts | 24 +++++ src/services/users/user-auth.ts | 2 +- src/services/users/user-signup.ts | 17 ++- 9 files changed, 241 insertions(+), 10 deletions(-) create mode 100644 src/middlewares/rate-limit.ts diff --git a/.env.example b/.env.example index e6bfc12..17902cc 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,12 @@ DB_HOST=postgres # Use this instead if you want to use locally # DB_HOST=localhost -DATABASE_URL=postgresql://:@${DB_HOST}:5432/${POSTGRES_DB}?schema= +DATABASE_URL=postgresql://:@${DB_HOST}:5432/${POSTGRES_DB}?schema= + +# Redis +REDIS_HOST=redis +REDIS_PORT=6379 +REDIS_PASSWORD=not_a_production_pass # Please change this # Express SERVER_PORT= diff --git a/README.md b/README.md index 683c649..6a3097b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ An open-source social media. **Client**: React, TailwindCSS and Radix UI (Primitives and Icons) -**Server**: ExpressJS, Jest, Docker, Postgresql, Prisma, Localstack, SWC and Typescript +**Server**: ExpressJS, Jest, Docker, Postgresql, Redis, Prisma, Localstack, SWC and Typescript ## To-do - Backend @@ -21,9 +21,9 @@ An open-source social media. - Like posts - Probably pinned posts - Authentication ✅ - - Add more verification (like, if the password is too short) + - Add more verification (like, if the password is too short) ✅ - Set display name ✅ -- Add rate limit +- Add rate limit ✅ ## Known problems diff --git a/docker-compose.yml b/docker-compose.yml index ccb383c..df1da8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: - 8080:8080 depends_on: - postgres + - redis env_file: - .env @@ -25,6 +26,18 @@ services: volumes: - postgres:/var/lib/postgresql/data + redis: + image: redis:alpine + restart: unless-stopped + container_name: redis + ports: + - 6379:6379 + command: redis-server --save 20 1 --loglevel warning --requirepass not_a_production_pass + volumes: + - redis:/data + volumes: postgres: - name: backend-db \ No newline at end of file + name: backend-db + redis: + driver: local \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 60c1198..c63b7f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,10 @@ "compression": "^1.7.4", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^6.7.1", + "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.0", + "rate-limit-redis": "^3.0.2", "validator": "^13.9.0" }, "devDependencies": { @@ -710,6 +713,11 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -3084,6 +3092,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "dev": true, @@ -3405,6 +3421,14 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -4378,6 +4402,17 @@ "node": ">= 0.10.0" } }, + "node_modules/express-rate-limit": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.1.tgz", + "integrity": "sha512-eH4VgI64Nowd2vC5Xylx0lLYovWIp2gRFtTklWDbhSDydGAPQUjvr1B7aQ2/ZADrAi6bJ51qSizKIXWAZ1WCQw==", + "engines": { + "node": ">= 14.0.0" + }, + "peerDependencies": { + "express": "^4 || ^5" + } + }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -5321,6 +5356,29 @@ "node": ">= 0.4" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ip": { "version": "1.1.8", "dev": true, @@ -6424,6 +6482,16 @@ "version": "4.17.21", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -7792,6 +7860,17 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-3.0.2.tgz", + "integrity": "sha512-4SBK6AzIr9PKkCF4HmSDcJH2O2KKMF3fZEcsbNMXyaL5I9d6X71uOreUldFRiyrRyP+qkQrTxzJ38ZKKN+sScw==", + "engines": { + "node": ">= 14.5.0" + }, + "peerDependencies": { + "express-rate-limit": "^6" + } + }, "node_modules/raw-body": { "version": "2.5.1", "license": "MIT", @@ -7866,6 +7945,25 @@ "node": ">=8.10.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "dev": true, @@ -8360,6 +8458,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/standard-engine": { "version": "15.1.0", "dev": true, @@ -10070,6 +10173,11 @@ "version": "1.2.1", "dev": true }, + "@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "@istanbuljs/load-nyc-config": { "version": "1.1.0", "dev": true, @@ -11650,6 +11758,11 @@ "mimic-response": "^1.0.0" } }, + "cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==" + }, "co": { "version": "4.6.0", "dev": true @@ -11851,6 +11964,11 @@ "delegates": { "version": "1.0.0" }, + "denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==" + }, "depd": { "version": "2.0.0" }, @@ -12486,6 +12604,12 @@ } } }, + "express-rate-limit": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.1.tgz", + "integrity": "sha512-eH4VgI64Nowd2vC5Xylx0lLYovWIp2gRFtTklWDbhSDydGAPQUjvr1B7aQ2/ZADrAi6bJ51qSizKIXWAZ1WCQw==", + "requires": {} + }, "ext-list": { "version": "2.2.2", "dev": true, @@ -13054,6 +13178,22 @@ "side-channel": "^1.0.4" } }, + "ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "requires": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + } + }, "ip": { "version": "1.1.8", "dev": true @@ -13776,6 +13916,16 @@ "lodash": { "version": "4.17.21" }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "lodash.memoize": { "version": "4.1.2", "dev": true @@ -14597,6 +14747,12 @@ "range-parser": { "version": "1.2.1" }, + "rate-limit-redis": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-3.0.2.tgz", + "integrity": "sha512-4SBK6AzIr9PKkCF4HmSDcJH2O2KKMF3fZEcsbNMXyaL5I9d6X71uOreUldFRiyrRyP+qkQrTxzJ38ZKKN+sScw==", + "requires": {} + }, "raw-body": { "version": "2.5.1", "requires": { @@ -14644,6 +14800,19 @@ "picomatch": "^2.2.1" } }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "requires": { + "redis-errors": "^1.0.0" + } + }, "regexp.prototype.flags": { "version": "1.5.0", "dev": true, @@ -14951,6 +15120,11 @@ } } }, + "standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "standard-engine": { "version": "15.1.0", "dev": true, diff --git a/package.json b/package.json index cbe641e..b7fca6a 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,10 @@ "compression": "^1.7.4", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-rate-limit": "^6.7.1", + "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.0", + "rate-limit-redis": "^3.0.2", "validator": "^13.9.0" } } diff --git a/src/controllers/users-router.ts b/src/controllers/users-router.ts index b2f700e..0399a90 100644 --- a/src/controllers/users-router.ts +++ b/src/controllers/users-router.ts @@ -11,13 +11,14 @@ import userUpdateController from './users/user-update' // Middlewares import ensureAuthenticated from '../middlewares/ensure-authenticated' +import limiter from '../middlewares/rate-limit' const usersRouter = Router() // Users related usersRouter.post('/auth', userAuthController) usersRouter.post('/delete', ensureAuthenticated, userDeleteController) -usersRouter.get('/info', userInfoController) +usersRouter.get('/info', limiter, userInfoController) usersRouter.post('/signup', userSignupController) usersRouter.put('/update', ensureAuthenticated, userUpdateController) diff --git a/src/middlewares/rate-limit.ts b/src/middlewares/rate-limit.ts new file mode 100644 index 0000000..8688002 --- /dev/null +++ b/src/middlewares/rate-limit.ts @@ -0,0 +1,24 @@ +import rateLimit from 'express-rate-limit' +import RedisStore from 'rate-limit-redis' +import RedisClient from 'ioredis' + +const redisPassword = process.env.REDIS_PASSWORD ?? '' +const redisHost = process.env.REDIS_HOST ?? '' +const redisPort = process.env.REDIS_PORT ?? '' + +const client = new RedisClient(`redis://:${redisPassword}@${redisHost}:${redisPort}/0`) + +const limiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 60 seconds + max: 5, + message: { error: 'Too many requests' }, + legacyHeaders: false, + + // Store configuration + store: new RedisStore({ + // @ts-expect-error - `call` function is not present in @types/ioredis + sendCommand: async (...args: string[]) => await client.call(...args) + }) +}) + +export default limiter diff --git a/src/services/users/user-auth.ts b/src/services/users/user-auth.ts index 9b2684c..f4aa859 100644 --- a/src/services/users/user-auth.ts +++ b/src/services/users/user-auth.ts @@ -17,7 +17,7 @@ async function userAuthService (email: string, password: string): Promise { if (username === undefined || email === undefined || password === undefined) { return new Error('Missing fields') } - if (!/^[a-zA-Z0-9_.]{5,15}$/.test(username)) { - return new Error('Username not allowed. Only alphanumerics characters (uppercase and lowercase words), underscore, dot and it must be between 5 and 15 characters') + if (!passwordRegex.test(password)) { + return new Error('Password must have 8 characters, one number and one special character.') + } + + if (password.trim().length < 8) { + return new Error('Password too short') + } + + if (!usernameRegex.test(username)) { + return new Error('Username not allowed. Only alphanumerics characters (uppercase and lowercase words), underscore, dots and it must be between 5 and 15 characters') } if (!validator.isEmail(email)) { @@ -24,7 +35,7 @@ async function userSignupService (username: string, email: string, password: str } const salt = await bcrypt.genSalt(15) - const hashedPassword = await bcrypt.hash(password, salt) + const hashedPassword = await bcrypt.hash(password.replace(/ /g, ''), salt) // Removes every space in the string const user = await prisma.user.create({ data: {