Implemented rate limit, added Redis database, added more verifications

This commit is contained in:
Hackntosh 2023-07-12 15:05:17 -03:00
parent be8dd3482a
commit f743521fae
9 changed files with 241 additions and 10 deletions

View file

@ -8,7 +8,12 @@ DB_HOST=postgres
# Use this instead if you want to use locally
# DB_HOST=localhost
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=<placeholder>
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=not_a_production_pass # Please change this
# Express
SERVER_PORT=<placeholder>

View file

@ -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

View file

@ -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
redis:
driver: local

174
package-lock.json generated
View file

@ -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,

View file

@ -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"
}
}

View file

@ -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)

View file

@ -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

View file

@ -17,7 +17,7 @@ async function userAuthService (email: string, password: string): Promise<Object
return new Error('Missing fields')
}
const validPassword = await bcrypt.compare(password, user.password)
const validPassword = await bcrypt.compare(password.replace(/ /g, ''), user.password)
if (!validPassword) {
return new Error('Invalid email or password')

View file

@ -2,13 +2,24 @@ import * as bcrypt from 'bcrypt'
import validator from 'validator'
import prisma from '../../db'
const passwordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$/
const usernameRegex = /^[a-zA-Z0-9_.]{5,15}$/
async function userSignupService (username: string, email: string, password: string): Promise<Object | Error> {
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: {