mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 17:41:15 +00:00
Added Multer and Sharp for image upload and compression.
This commit is contained in:
parent
cb9cf02614
commit
4382b35fe3
22 changed files with 5864 additions and 537 deletions
25
.env.example
25
.env.example
|
@ -1,22 +1,33 @@
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
# Postgres config
|
# Postgres config
|
||||||
POSTGRES_DB=<placeholder>
|
POSTGRES_DB=<placeholder>
|
||||||
POSTGRES_USER=<placeholder>
|
POSTGRES_USER=<placeholder>
|
||||||
POSTGRES_PASSWORD=<placeholder>
|
POSTGRES_PASSWORD=<placeholder>
|
||||||
# POSTGRES CONTAINER NAME
|
|
||||||
DB_HOST=postgres
|
|
||||||
|
|
||||||
# Use this instead if you want to use locally
|
# Docker DB host
|
||||||
# DB_HOST=localhost
|
# DB_HOST=postgres
|
||||||
|
|
||||||
|
DB_HOST=localhost
|
||||||
|
|
||||||
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=<placeholder>
|
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=<placeholder>
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=127.0.0.1 # localhost (for docker use redis)
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_PASSWORD=not_a_production_pass # Please change this
|
REDIS_PASSWORD=<placehoder>
|
||||||
|
|
||||||
# Express
|
# Express
|
||||||
SERVER_PORT=<placeholder>
|
SERVER_PORT=<placeholder>
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_ACCESS_SECRET=<placeholder>
|
JWT_ACCESS_SECRET=<placeholder>
|
||||||
|
|
||||||
|
# Localstack - The data can be fake (Change this to real data on production)
|
||||||
|
DOCKER_HOST=unix:///var/run/docker.sock
|
||||||
|
SERVICES=s3
|
||||||
|
AWS_ACCESS_KEY_ID=<placeholder>
|
||||||
|
AWS_SECRET_ACCESS_KEY=<placeholder>
|
||||||
|
AWS_DEFAULT_REGION=us-east-1
|
||||||
|
AWS_DEFAULT_OUTPUT=json
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -6,4 +6,5 @@ prisma/*.db
|
||||||
prisma/migrations/dev
|
prisma/migrations/dev
|
||||||
client
|
client
|
||||||
dist
|
dist
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
package_backup.json
|
10
README.md
10
README.md
|
@ -12,11 +12,13 @@ An open-source social media.
|
||||||
## To-do - Backend
|
## To-do - Backend
|
||||||
|
|
||||||
- Create/update/delete Posts ✅
|
- Create/update/delete Posts ✅
|
||||||
|
- Add post attachments
|
||||||
- Create/update/delete Users ✅
|
- Create/update/delete Users ✅
|
||||||
- Password recuperation
|
- Password recuperation
|
||||||
- Two step verification
|
- Two step verification
|
||||||
- Able to choose a profile picture
|
- Able to choose a profile picture✅
|
||||||
- Probably gonna use LocalStack to mock Amazon S3
|
- Probably gonna use LocalStack to mock Amazon S3✅
|
||||||
|
- Image compression ✅
|
||||||
- Following/unfollowing features
|
- Following/unfollowing features
|
||||||
- Like posts
|
- Like posts
|
||||||
- Probably pinned posts
|
- Probably pinned posts
|
||||||
|
@ -25,10 +27,6 @@ An open-source social media.
|
||||||
- Set display name ✅
|
- Set display name ✅
|
||||||
- Add rate limit ✅
|
- Add rate limit ✅
|
||||||
|
|
||||||
## Known problems
|
|
||||||
|
|
||||||
- Tests taking too long
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[MIT](https://choosealicense.com/licenses/mit/)
|
[MIT](https://choosealicense.com/licenses/mit/)
|
||||||
|
|
|
@ -11,6 +11,16 @@ services:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
- postgres:/var/lib/postgresql/data
|
- 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:
|
volumes:
|
||||||
postgres:
|
postgres:
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
localstack-net:
|
||||||
|
name: localstack-net
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
api:
|
api:
|
||||||
container_name: api
|
container_name: api
|
||||||
|
@ -12,6 +17,7 @@ services:
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- redis
|
- redis
|
||||||
|
- localstack
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
|
@ -25,6 +31,21 @@ services:
|
||||||
- .env
|
- .env
|
||||||
volumes:
|
volumes:
|
||||||
- postgres:/var/lib/postgresql/data
|
- postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
|
localstack:
|
||||||
|
image: localstack/localstack
|
||||||
|
container_name: localstack_main
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- localstack-net
|
||||||
|
ports:
|
||||||
|
- 4566:4566
|
||||||
|
- 4572:4572
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- localstack:/data
|
||||||
|
- "/var/run/docker.sock:/var/run/docker.sock"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
@ -40,4 +61,5 @@ volumes:
|
||||||
postgres:
|
postgres:
|
||||||
name: backend-db
|
name: backend-db
|
||||||
redis:
|
redis:
|
||||||
driver: local
|
driver: local
|
||||||
|
localstack:
|
6006
package-lock.json
generated
6006
package-lock.json
generated
File diff suppressed because it is too large
Load diff
14
package.json
14
package.json
|
@ -15,6 +15,7 @@
|
||||||
"prisma:generate": "npx prisma generate",
|
"prisma:generate": "npx prisma generate",
|
||||||
"prisma:seed": "prisma db seed",
|
"prisma:seed": "prisma db seed",
|
||||||
"prisma:studio": "npx prisma studio",
|
"prisma:studio": "npx prisma studio",
|
||||||
|
"prisma:deploy": "npx prisma migrate deploy",
|
||||||
"prod:start": "pm2-runtime start dist/server.js",
|
"prod:start": "pm2-runtime start dist/server.js",
|
||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
|
@ -38,14 +39,15 @@
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/jsonwebtoken": "^9.0.2",
|
"@types/jsonwebtoken": "^9.0.2",
|
||||||
|
"@types/multer-s3": "^3.0.0",
|
||||||
"@types/node": "^20.3.1",
|
"@types/node": "^20.3.1",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/validator": "^13.7.17",
|
"@types/validator": "^13.7.17",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^3.0.1",
|
||||||
"pm2": "^5.3.0",
|
"pm2": "^4.2.3",
|
||||||
"prisma": "^4.16.0",
|
"prisma": "^5.0.0",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-node-dev": "^2.0.0",
|
"ts-node-dev": "^2.0.0",
|
||||||
|
@ -53,7 +55,8 @@
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^4.16.0",
|
"@prisma/client": "^5.0.0",
|
||||||
|
"aws-sdk": "^2.1414.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
@ -61,7 +64,10 @@
|
||||||
"express-rate-limit": "^6.7.1",
|
"express-rate-limit": "^6.7.1",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"multer-s3": "^3.0.1",
|
||||||
"rate-limit-redis": "^3.0.2",
|
"rate-limit-redis": "^3.0.2",
|
||||||
|
"sharp": "^0.32.3",
|
||||||
"validator": "^13.9.0"
|
"validator": "^13.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "pfpUrl" TEXT;
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `pfpUrl` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "pfpUrl",
|
||||||
|
ADD COLUMN "profilePicture" TEXT;
|
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `profilePicture` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "profilePicture",
|
||||||
|
ADD COLUMN "profileImage" TEXT;
|
|
@ -11,13 +11,14 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
displayName String?
|
displayName String?
|
||||||
username String @unique
|
username String @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
posts Post[]
|
posts Post[]
|
||||||
createdAt DateTime @default(now())
|
profileImage String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Post {
|
model Post {
|
||||||
|
|
|
@ -3,12 +3,20 @@ import 'dotenv/config'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
import router from './routes'
|
import router from './routes'
|
||||||
import compression from 'compression'
|
import compression from 'compression'
|
||||||
|
import limiter from './middlewares/rate-limit'
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
|
// TODO: Disable image resize when it's a post attachment
|
||||||
|
// TODO: Add user-upload-picture tests
|
||||||
|
// TODO: Apply http-errors lib on the controllers
|
||||||
|
// TODO: Automatically apply the newest migration when starting up the docker
|
||||||
|
// TODO: Refactor some parts of the code
|
||||||
|
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(limiter)
|
||||||
app.use(compression({ level: 9 }))
|
app.use(compression({ level: 9 }))
|
||||||
|
|
||||||
app.use((_req, res) => {
|
app.use((_req, res) => {
|
||||||
|
|
23
src/config/clients/s3-client.ts
Normal file
23
src/config/clients/s3-client.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { S3Client } from '@aws-sdk/client-s3'
|
||||||
|
|
||||||
|
let s3: S3Client
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('Using Localstack services instead of AWS.')
|
||||||
|
s3 = new S3Client({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? ''
|
||||||
|
},
|
||||||
|
endpoint: 'http://127.0.0.1:4566' // Uses localstack instead of aws, make sure to create the bucket first with public-read acl
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
s3 = new S3Client({
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default s3
|
64
src/config/multer.ts
Normal file
64
src/config/multer.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import multer from 'multer'
|
||||||
|
import { Request } from 'express'
|
||||||
|
import path from 'path'
|
||||||
|
import s3 from './clients/s3-client'
|
||||||
|
import multerS3 from 'multer-s3'
|
||||||
|
|
||||||
|
const tempFolder = path.resolve(__dirname, '..', '..', 'temp', 'uploads')
|
||||||
|
|
||||||
|
const storageTypes = {
|
||||||
|
local: multer.diskStorage({
|
||||||
|
destination: (req: Request, file: Express.Multer.File, callback) => {
|
||||||
|
callback(null, tempFolder)
|
||||||
|
},
|
||||||
|
|
||||||
|
filename: (req: Request, file: Express.Multer.File, callback) => {
|
||||||
|
/* eslint-disable */
|
||||||
|
const folder = req.body.isProfilePicture ? 'profile_images' : 'media'
|
||||||
|
const fileName: string = `${folder}/${req.user!.id}.webp`
|
||||||
|
|
||||||
|
callback(null, fileName)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
s3: multerS3({
|
||||||
|
s3,
|
||||||
|
bucket: process.env.AWS_BUCKET ?? '',
|
||||||
|
contentType: multerS3.AUTO_CONTENT_TYPE,
|
||||||
|
acl: 'public-read',
|
||||||
|
key: (req: Request, file: Express.Multer.File, callback) => {
|
||||||
|
let folder
|
||||||
|
|
||||||
|
if (req.body.isProfilePicture === 'true') {
|
||||||
|
folder = 'profile_images'
|
||||||
|
} else {
|
||||||
|
folder = 'media'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName: string = `${folder}/${req.user!.id}.jpg`
|
||||||
|
callback(null, fileName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const multerConfig = {
|
||||||
|
dest: tempFolder,
|
||||||
|
storage: storageTypes.s3,
|
||||||
|
limits: {
|
||||||
|
fileSize: 15 * 1024 * 1024 // 1mb
|
||||||
|
},
|
||||||
|
fileFilter: (req: Request, file: Express.Multer.File, callback: multer.FileFilterCallback) => {
|
||||||
|
const allowedMimes = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png'
|
||||||
|
]
|
||||||
|
|
||||||
|
if (allowedMimes.includes(file.mimetype)) {
|
||||||
|
callback(null, true)
|
||||||
|
} else {
|
||||||
|
callback(new Error('Filetype not allowed'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default multerConfig
|
|
@ -8,18 +8,20 @@ import userDeleteController from './users/user-delete'
|
||||||
import userInfoController from './users/user-info'
|
import userInfoController from './users/user-info'
|
||||||
import userSignupController from './users/user-signup'
|
import userSignupController from './users/user-signup'
|
||||||
import userUpdateController from './users/user-update'
|
import userUpdateController from './users/user-update'
|
||||||
|
import userUploadPictureController from './users/user-upload-picture'
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
import ensureAuthenticated from '../middlewares/ensure-authenticated'
|
import ensureAuthenticated from '../middlewares/ensure-authenticated'
|
||||||
import limiter from '../middlewares/rate-limit'
|
import uploadFile from '../middlewares/upload-image'
|
||||||
|
|
||||||
const usersRouter = Router()
|
const usersRouter = Router()
|
||||||
|
|
||||||
// Users related
|
// Users related
|
||||||
usersRouter.post('/auth', userAuthController)
|
usersRouter.post('/auth', userAuthController)
|
||||||
usersRouter.post('/delete', ensureAuthenticated, userDeleteController)
|
usersRouter.post('/delete', ensureAuthenticated, userDeleteController)
|
||||||
usersRouter.get('/info', limiter, userInfoController)
|
usersRouter.get('/info', userInfoController)
|
||||||
usersRouter.post('/signup', userSignupController)
|
usersRouter.post('/signup', userSignupController)
|
||||||
usersRouter.put('/update', ensureAuthenticated, userUpdateController)
|
usersRouter.put('/update', ensureAuthenticated, userUpdateController)
|
||||||
|
usersRouter.put('/profile-picture/upload', ensureAuthenticated, uploadFile, userUploadPictureController)
|
||||||
|
|
||||||
export default usersRouter
|
export default usersRouter
|
||||||
|
|
32
src/controllers/users/user-upload-picture.ts
Normal file
32
src/controllers/users/user-upload-picture.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import userUploadPictureService from '../../services/users/upload-picture'
|
||||||
|
import { Request, Response } from 'express'
|
||||||
|
import { badRequest } from '../../lib/http-errors'
|
||||||
|
|
||||||
|
let url
|
||||||
|
|
||||||
|
async function userUploadPictureController (req: Request, res: Response): Promise<void> {
|
||||||
|
if (req.file === undefined) {
|
||||||
|
return badRequest(res, 'Expected a JPG or PNG file')
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = req.user?.id ?? ''
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-expect-error property `key` doesn't exists in @types/express
|
||||||
|
url = `http://${process.env.AWS_BUCKET ?? ''}.s3.localhost.localstack.cloud:4566/${req.file.key}`
|
||||||
|
} else {
|
||||||
|
// @ts-expect-error property `location` doesn't exists in @types/express
|
||||||
|
url = req.file.location
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await userUploadPictureService(userId, url)
|
||||||
|
|
||||||
|
if (result instanceof Error) {
|
||||||
|
return badRequest(res, result.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default userUploadPictureController
|
35
src/lib/compress-image.ts
Normal file
35
src/lib/compress-image.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import sharp from 'sharp'
|
||||||
|
import s3 from '../config/clients/s3-client'
|
||||||
|
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||||
|
|
||||||
|
export default async function compressImage (imageName: string): Promise<Error | Object > {
|
||||||
|
// Get file from s3
|
||||||
|
const { Body } = await s3.send(new GetObjectCommand({
|
||||||
|
Bucket: process.env.AWS_BUCKET ?? '',
|
||||||
|
Key: imageName
|
||||||
|
}))
|
||||||
|
|
||||||
|
const imageBuffer = await Body?.transformToByteArray()
|
||||||
|
|
||||||
|
const compressedImageBuffer = await sharp(imageBuffer)
|
||||||
|
.resize(200, 200)
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.toBuffer()
|
||||||
|
|
||||||
|
// Send file back
|
||||||
|
const params = {
|
||||||
|
Bucket: process.env.AWS_BUCKET ?? '',
|
||||||
|
Key: imageName,
|
||||||
|
Body: compressedImageBuffer,
|
||||||
|
ContentType: 'image/jpeg',
|
||||||
|
ContentDisposition: 'inline'
|
||||||
|
}
|
||||||
|
|
||||||
|
const { ETag } = await s3.send(new PutObjectCommand(params))
|
||||||
|
|
||||||
|
if (ETag !== null) {
|
||||||
|
return {}
|
||||||
|
} else {
|
||||||
|
return new Error('Error while compressing image')
|
||||||
|
}
|
||||||
|
}
|
21
src/lib/http-errors.ts
Normal file
21
src/lib/http-errors.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { Response } from 'express'
|
||||||
|
|
||||||
|
const sendErrorResponse = (res: Response, status: number, message: string): void => {
|
||||||
|
res.status(status).json({ error: message })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const badRequest = (res: Response, message = 'Bad Request'): void => {
|
||||||
|
sendErrorResponse(res, 400, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unauthorized = (res: Response, message = 'Unauthorized'): void => {
|
||||||
|
sendErrorResponse(res, 401, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const forbidden = (res: Response, message = 'Forbidden'): void => {
|
||||||
|
sendErrorResponse(res, 403, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const internalServerError = (res: Response, message = 'Internal Server Error'): void => {
|
||||||
|
sendErrorResponse(res, 500, message)
|
||||||
|
}
|
|
@ -8,9 +8,18 @@ const redisPort = process.env.REDIS_PORT ?? ''
|
||||||
|
|
||||||
const client = new RedisClient(`redis://:${redisPassword}@${redisHost}:${redisPort}/0`)
|
const client = new RedisClient(`redis://:${redisPassword}@${redisHost}:${redisPort}/0`)
|
||||||
|
|
||||||
|
let maxConnections
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.log('Detected Development environment, disabling rate limiter.')
|
||||||
|
maxConnections = 0
|
||||||
|
} else {
|
||||||
|
maxConnections = 5
|
||||||
|
}
|
||||||
|
|
||||||
const limiter = rateLimit({
|
const limiter = rateLimit({
|
||||||
windowMs: 1 * 60 * 1000, // 60 seconds
|
windowMs: 1 * 60 * 1000, // 60 seconds
|
||||||
max: 5,
|
max: maxConnections,
|
||||||
message: { error: 'Too many requests' },
|
message: { error: 'Too many requests' },
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
|
|
||||||
|
|
37
src/middlewares/upload-image.ts
Normal file
37
src/middlewares/upload-image.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
import { Response, Request, NextFunction } from 'express'
|
||||||
|
import multer from 'multer'
|
||||||
|
import multerConfig from '../config/multer'
|
||||||
|
import compressImage from '../lib/compress-image'
|
||||||
|
|
||||||
|
function uploadImage (req: Request, res: Response, next: NextFunction) {
|
||||||
|
const upload = multer(multerConfig).single('image')
|
||||||
|
|
||||||
|
upload(req, res, async (cb: multer.MulterError | Error | any) => {
|
||||||
|
if (req.user == null) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'You must be logged in to upload a profile picture'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cb instanceof multer.MulterError || cb instanceof Error) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: cb.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error property `key` does not exists in types
|
||||||
|
const compressed = await compressImage(req.file?.key)
|
||||||
|
|
||||||
|
if (compressed instanceof Error) {
|
||||||
|
return res.status(500).json({
|
||||||
|
error: compressed.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default uploadImage
|
36
src/services/users/upload-picture.ts
Normal file
36
src/services/users/upload-picture.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import prisma from '../../db'
|
||||||
|
|
||||||
|
async function userUploadPictureService (authorId: string, url: string): Promise<Object | Error> {
|
||||||
|
const user = await prisma.user.findFirst({ where: { id: authorId } })
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return new Error('User does not exists')
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: authorId
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
profileImage: url
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
profileImage: true,
|
||||||
|
displayName: true,
|
||||||
|
username: true,
|
||||||
|
createdAt: true,
|
||||||
|
posts: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return updatedUser
|
||||||
|
}
|
||||||
|
|
||||||
|
export default userUploadPictureService
|
|
@ -6,6 +6,7 @@ async function userInfoService (username: string): Promise<Object> {
|
||||||
username
|
username
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
profileImage: true,
|
||||||
displayName: true,
|
displayName: true,
|
||||||
username: true,
|
username: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
|
|
Loading…
Reference in a new issue