Added Cookies for authentication.

This commit is contained in:
Hackntosh 2023-08-22 16:57:34 -03:00
parent ffcb719da1
commit 2607bd56ff
35 changed files with 113 additions and 5152 deletions

View file

@ -1,17 +0,0 @@
import { pathsToModuleNameMapper } from 'ts-jest'
import { compilerOptions } from './tsconfig.json'
import type { JestConfigWithTsJest } from 'ts-jest'
const jestConfig: JestConfigWithTsJest = {
preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/setupTest.ts'],
transform: {
'^.+\\.(t|j)sx?$': '@swc/jest'
},
roots: ['<rootDir>'],
modulePaths: [compilerOptions.baseUrl],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths)
}
export default jestConfig

4735
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -16,8 +16,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",
"prod:start": "npx prisma migrate deploy && pm2-runtime start dist/server.js", "prod:start": "npx prisma migrate deploy && pm2-runtime start dist/server.js"
"test": "jest"
}, },
"ts-standard": { "ts-standard": {
"project": "tsconfig.json", "project": "tsconfig.json",
@ -32,13 +31,12 @@
"@faker-js/faker": "^8.0.2", "@faker-js/faker": "^8.0.2",
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",
"@swc/core": "^1.3.66", "@swc/core": "^1.3.66",
"@swc/jest": "^0.2.26",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/compression": "^1.7.2", "@types/compression": "^1.7.2",
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.13", "@types/cors": "^2.8.13",
"@types/dotenv": "^8.2.0", "@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/jsonwebtoken": "^9.0.2", "@types/jsonwebtoken": "^9.0.2",
"@types/morgan": "^1.9.4", "@types/morgan": "^1.9.4",
"@types/multer-s3": "^3.0.0", "@types/multer-s3": "^3.0.0",
@ -53,13 +51,11 @@
"eslint-plugin-import": "^2.28.0", "eslint-plugin-import": "^2.28.0",
"eslint-plugin-n": "^16.0.1", "eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1", "eslint-plugin-promise": "^6.1.1",
"jest": "^29.6.2",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"pm2": "^4.2.3", "pm2": "^4.2.3",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"prisma": "^5.0.0", "prisma": "^5.0.0",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.1.6" "typescript": "^5.1.6"
@ -69,6 +65,7 @@
"aws-sdk": "^2.1414.0", "aws-sdk": "^2.1414.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"compression": "^1.7.4", "compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",

View file

@ -1,9 +0,0 @@
import prisma from './src/clients/prisma-client'
import redis from './src/clients/redis-client'
process.env.NODE_ENV = 'development'
afterAll(async () => {
await redis.disconnect()
await prisma.$disconnect()
})

View file

@ -1,12 +1,8 @@
/* eslint-disable */ /* eslint-disable */
import * as express from 'express' import * as express from 'express'
import type jwtPayload from '../interfaces/jwt'
declare global { declare global {
namespace Express { namespace Express {
interface Request {
user: jwtPayload | undefined
}
namespace Multer { namespace Multer {
interface File { interface File {
location: string location: string

View file

@ -6,6 +6,7 @@ import express from 'express'
import limiter from 'middlewares/rate-limit' import limiter from 'middlewares/rate-limit'
import morganMiddleware from 'middlewares/morgan' import morganMiddleware from 'middlewares/morgan'
import router from './routes' import router from './routes'
import cookieParser from 'cookie-parser'
const app = express() const app = express()
@ -16,6 +17,7 @@ const app = express()
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: true })) app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
app.use(morganMiddleware) app.use(morganMiddleware)
app.use(limiter) app.use(limiter)
app.use(router) app.use(router)

View file

@ -15,7 +15,7 @@ const storageTypes = {
filename: (req: Request, file: Express.Multer.File, callback) => { filename: (req: Request, file: Express.Multer.File, callback) => {
/* eslint-disable */ /* eslint-disable */
const folder = req.body.isProfilePicture ? 'profile_images' : 'media' const folder = req.body.isProfilePicture ? 'profile_images' : 'media'
const fileName: string = `${folder}/${req.user!.id}.webp` const fileName: string = `${folder}/${req.res?.locals.user.id}.webp`
callback(null, fileName) callback(null, fileName)
}, },
@ -35,7 +35,7 @@ const storageTypes = {
folder = 'media' folder = 'media'
} }
const fileName: string = `${folder}/${req.user!.id}.jpg` const fileName: string = `${folder}/${req.res?.locals.user.id}.jpg`
callback(null, fileName) callback(null, fileName)
}, },
}), }),

View file

@ -8,7 +8,7 @@ async function commentCreateController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { content, postId } = req.body const { content, postId } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
if (postId === undefined) { if (postId === undefined) {
badRequest(res, 'Expected post id'); return badRequest(res, 'Expected post id'); return

View file

@ -8,7 +8,7 @@ async function commentDeleteController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { commentId } = req.body const { commentId } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
if (commentId === undefined) { if (commentId === undefined) {
badRequest(res, 'Expected comment id'); return badRequest(res, 'Expected comment id'); return

View file

@ -8,7 +8,7 @@ async function commentUpdateController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { commentId, content } = req.body const { commentId, content } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
if (commentId === undefined) { if (commentId === undefined) {
badRequest(res, 'Expected comment content'); return badRequest(res, 'Expected comment content'); return

View file

@ -8,7 +8,7 @@ async function postCreateController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { content } = req.body const { content } = req.body
const id: string = req.user?.id ?? '' const id = res.locals.user.id
if (content === undefined) { if (content === undefined) {
badRequest(res, 'Expected post content'); return badRequest(res, 'Expected post content'); return

View file

@ -7,7 +7,7 @@ async function postDeleteController (
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const postId = req.body.postId const postId = req.body.postId
if (postId === undefined) { if (postId === undefined) {

View file

@ -7,7 +7,7 @@ async function postUpdateController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { postId, content } = req.body const { postId, content } = req.body
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const result = await post.update(postId, content, userId) const result = await post.update(postId, content, userId)

View file

@ -1,13 +1,21 @@
import user from 'services/users' import user from 'services/users'
import type { Request, Response } from 'express' import type { Request, Response } from 'express'
import handleResponse from 'helpers/handle-response' import { badRequest } from 'helpers/http-errors'
async function userAuthController (req: Request, res: Response): Promise<void> { async function userAuthController (req: Request, res: Response): Promise<void> {
const { email, password } = req.body const { email, password } = req.body
const result = await user.auth({ email, password }) const result = await user.auth({ email, password })
handleResponse(res, result) if (result instanceof Error) {
badRequest(res, result.message)
} else {
res.cookie('token', result.token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production'
}).status(200)
.json({ message: 'Logged in successfully' })
}
} }
export default userAuthController export default userAuthController

View file

@ -6,7 +6,7 @@ async function userDeleteController (
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const result = await user.delete(userId) const result = await user.delete(userId)
handleResponse(res, result) handleResponse(res, result)

View file

@ -6,7 +6,7 @@ async function userFollowController (
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const { userToFollow } = req.body const { userToFollow } = req.body
const result = await user.follow(userId, userToFollow) const result = await user.follow(userId, userToFollow)

View file

@ -6,7 +6,7 @@ async function userLikeCommentController (
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const { commentId } = req.body const { commentId } = req.body
const result = await user.likeComment(commentId, userId) const result = await user.likeComment(commentId, userId)

View file

@ -6,7 +6,7 @@ async function userLikePostController (
req: Request, req: Request,
res: Response res: Response
): Promise<void> { ): Promise<void> {
const userId = req.user?.id ?? '' const userId = res.locals.user.id
const { postId } = req.body const { postId } = req.body
const result = await user.likePost(postId, userId) const result = await user.likePost(postId, userId)

View file

@ -7,7 +7,7 @@ async function userUpdateEmailController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { email } = req.body const { email } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
const result = await user.updateEmail({ id, email }) const result = await user.updateEmail({ id, email })

View file

@ -7,7 +7,7 @@ async function userUpdateNameController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { displayName, username } = req.body const { displayName, username } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
const result = await user.updateName({ id, displayName, username }) const result = await user.updateName({ id, displayName, username })

View file

@ -7,7 +7,7 @@ async function userUpdatePasswordController (
res: Response res: Response
): Promise<void> { ): Promise<void> {
const { currentPassword, newPassword } = req.body const { currentPassword, newPassword } = req.body
const id = req.user?.id ?? '' const id = res.locals.user.id
const result = await user.updatePassword(id, currentPassword, newPassword) const result = await user.updatePassword(id, currentPassword, newPassword)

View file

@ -12,7 +12,7 @@ async function userUploadPictureController (
badRequest(res, 'Expected a JPG or PNG file'); return badRequest(res, 'Expected a JPG or PNG file'); return
} }
const userId = req.user?.id ?? '' const userId = res.locals.user.id
let url: string let url: string

View file

@ -1,55 +1,41 @@
import { verify } from 'jsonwebtoken' import { verify } from 'jsonwebtoken'
import prisma from 'clients/prisma-client' import prisma from 'clients/prisma-client'
import type { Response, Request, NextFunction } from 'express' import type { Response, Request, NextFunction } from 'express'
import type jwtPayload from 'interfaces/jwt'
import { unauthorized } from 'helpers/http-errors' import { unauthorized } from 'helpers/http-errors'
import type jwtPayload from 'interfaces/jwt'
async function authenticated ( async function authenticated (
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
): Promise<void> { ): Promise<void> {
if ( try {
req.headers.authorization === undefined || const token = req.cookies.token
req.headers.authorization.length === 0
) { if (token === undefined) {
unauthorized(res, 'Missing token'); return unauthorized(res, 'Missing token'); return
} }
const token = req.headers.authorization.split(' ')[1] const { id } = verify(token, process.env.JWT_ACCESS_SECRET ?? '') as jwtPayload
try { if (id === undefined) {
const decoded = await new Promise<jwtPayload | undefined>(
(resolve, reject) => {
verify(token, process.env.JWT_ACCESS_SECRET ?? '', (error, decoded) => {
if (error != null) {
reject(error)
} else {
resolve(decoded as jwtPayload)
}
})
}
)
if (decoded == null) {
unauthorized(res, 'Invalid token'); return unauthorized(res, 'Invalid token'); return
} }
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { where: {
id: decoded.id id
} }
}) })
if (user == null) { if (user === undefined) {
unauthorized(res, 'User does not exists'); return unauthorized(res, 'User does not exists'); return
} }
req.user = decoded res.locals.user = user
next() next()
} catch (error) { } catch (e) {
unauthorized(res, `JWT Error: ${(error as Error).message}`) unauthorized(res, `JWT Error: ${(e as Error).message}`)
} }
} }

View file

@ -8,7 +8,7 @@ function uploadImage (req: Request, res: Response, next: NextFunction): void {
const upload = multer(multerConfig).single('image') const upload = multer(multerConfig).single('image')
upload(req, res, async (cb: multer.MulterError | Error | any) => { upload(req, res, async (cb: multer.MulterError | Error | any) => {
if (req.user == null) { if (req.res?.locals.user == null) {
badRequest( badRequest(
res, res,
'You must be logged in to upload a profile picture' 'You must be logged in to upload a profile picture'

View file

@ -3,6 +3,9 @@ import { createServer } from 'http'
import logger from 'helpers/logger' import logger from 'helpers/logger'
import createSocketIOInstance from './socket' import createSocketIOInstance from './socket'
import prisma from 'clients/prisma-client'
import redis from 'clients/redis-client'
const server = createServer(app) const server = createServer(app)
const io = createSocketIOInstance(server) const io = createSocketIOInstance(server)
@ -15,3 +18,10 @@ app.use((req, res, next) => {
server.listen(process.env.SERVER_PORT, () => { server.listen(process.env.SERVER_PORT, () => {
logger.info(`Server is running @ ${process.env.SERVER_PORT ?? ''}`) logger.info(`Server is running @ ${process.env.SERVER_PORT ?? ''}`)
}) })
process.on('SIGINT', async () => {
logger.warn('Closing server...')
await prisma.$disconnect()
await redis.disconnect()
server.close()
})

View file

@ -1,44 +0,0 @@
import app from '../../app'
import request from 'supertest'
import signUpNewUser from '../utils/create-user'
import deleteUser from '../utils/delete-user'
import type User from 'interfaces/user'
let user: User
describe('POST /post/create', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should respond with 200 status code if the user send the token and the content', async () => {
const response = await request(app)
.post('/post/create')
.send({
content: 'Hello world'
})
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
expect(response.body).toEqual(
expect.objectContaining({
id: expect.any(String),
content: expect.any(String),
authorId: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String)
})
)
})
it('should respond with 400 status code if the user send no token', async () => {
const response = await request(app).post('/post/create').expect(401)
expect(response.body).toHaveProperty('error')
})
})

View file

@ -1,36 +0,0 @@
import app from '../../app'
import request from 'supertest'
import signUpNewUser from '../utils/create-user'
import deleteUser from '../utils/delete-user'
import type User from 'interfaces/user'
let user: User
describe('DELETE /post/delete', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should delete the post successfully', async () => {
const response = await request(app)
.post('/post/create')
.send({
content: 'lorem ipsum'
})
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
await request(app)
.post('/post/delete')
.send({
postId: response.body.id
})
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
})
})

View file

@ -1,54 +0,0 @@
import app from '../../app'
import request from 'supertest'
import signUpNewUser from '../utils/create-user'
import deleteUser from '../utils/delete-user'
import type User from 'interfaces/user'
let postId: string
let user: User
describe('POST /post/info', () => {
beforeAll(async () => {
user = await signUpNewUser()
const token = user.token ?? ''
const post = await request(app)
.post('/post/create')
.send({
content: 'Hello world'
})
.set('Authorization', `Bearer ${token}`)
.expect(200)
postId = post.body.id
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should respond with 200 status code and return some info about the post', async () => {
const response = await request(app)
.get(`/post/info?id=${postId}`)
.expect(200)
expect(response.body).toEqual(
expect.objectContaining({
id: expect.any(String),
content: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
author: expect.any(Object)
})
)
})
it('should respond with 400 status code if the post does not exists', async () => {
const response = await request(app).get('/post/info?id=abc').expect(400)
expect(response.body).toHaveProperty('error')
})
})

View file

@ -1,56 +0,0 @@
import app from '../../app'
import request from 'supertest'
import signUpNewUser from '../utils/create-user'
import deleteUser from '../utils/delete-user'
import type User from 'interfaces/user'
let user: User
describe('PUT /post/update', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should create a new post and update the content of it', async () => {
const post = await request(app)
.post('/post/create')
.send({
content: 'Lorem'
})
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
expect(post.body).toHaveProperty('id')
const fieldsToUpdate = {
postId: post.body.id,
content: 'Lorem ipsum'
}
const response = await request(app)
.put('/post/update')
.send(fieldsToUpdate)
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
// Post content should be Lorem Ipsum
if (post.body.content === response.body.content) {
throw new Error("Post didn't update")
}
expect(response.body).toEqual(
expect.objectContaining({
id: expect.any(String),
content: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
author: expect.any(Object)
})
)
})
})

View file

@ -1,45 +0,0 @@
import request from 'supertest'
import app from '../../app'
import deleteUser from '../utils/delete-user'
import signUpNewUser from '../utils/create-user'
import type User from 'interfaces/user'
let user: User
describe('POST /user/auth', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should respond with a error if the user does not exists', async () => {
const response = await request(app)
.post('/user/auth')
.send({ email: 'mm@mm.com', password: 'aa' })
.expect(400)
expect(response.body).toHaveProperty('error')
expect(response.body.error).toBe('Invalid email or password')
})
it('should respond with a error if receive an invalid email or password', async () => {
const response = await request(app)
.post('/user/auth')
.send({ email: user.email, password: 'fake_pass' })
.expect(400)
expect(response.body).toHaveProperty('error')
expect(response.body.error).toBe('Invalid email or password')
})
it('should respond with a error if receive an empty body', async () => {
const response = await request(app).post('/user/auth').send({}).expect(400)
expect(response.body).toHaveProperty('error')
expect(response.body.error).toBe('Missing fields')
})
})

View file

@ -1,19 +0,0 @@
import app from '../../app'
import request from 'supertest'
import signUpNewUser from '../utils/create-user'
import type User from 'interfaces/user'
let user: User
describe('DELETE /user/delete', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
it('should delete the user successfully', async () => {
await request(app)
.post('/user/delete')
.set('Authorization', `Bearer ${user.token ?? ''}`)
.expect(200)
})
})

View file

@ -1,36 +0,0 @@
import app from '../../app'
import request from 'supertest'
import deleteUser from '../utils/delete-user'
import signUpNewUser from '../utils/create-user'
import type User from 'interfaces/user'
let user: User
describe('POST /user/info', () => {
beforeAll(async () => {
user = await signUpNewUser()
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should respond with 200 status code and return the user data', async () => {
const response = await request(app)
.get(`/user/info?u=${user.username ?? ''}`)
.expect(200)
expect(response.body).toHaveProperty('profileImage')
expect(response.body).toHaveProperty('displayName')
expect(response.body).toHaveProperty('username')
expect(response.body).toHaveProperty('createdAt')
expect(response.body).toHaveProperty('posts')
})
it('should respond with 400 status code if the user send no username', async () => {
const response = await request(app).get('/user/info?u=').expect(400)
expect(response.body).toHaveProperty('error')
})
})

View file

@ -1,45 +0,0 @@
import request from 'supertest'
import app from '../../app'
import deleteUser from '../utils/delete-user'
import signUpNewUser from '../utils/create-user'
import type User from 'interfaces/user'
let user: User
describe('POST /user/signup', () => {
beforeAll(async () => {
user = await signUpNewUser()
delete user.token
})
afterAll(async () => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await deleteUser(user.username!)
})
it('should respond with a 400 status code if sent any invalid data', async () => {
await request(app)
.post('/user/signup')
.send({
username: 'username12@',
email: user.email,
password: user.password
})
.expect(400)
})
it('should respond with a 400 status code for an existing username or email', async () => {
await request(app)
.post('/user/signup')
.send({
username: user.username,
email: user.email,
password: user.password
})
.expect(400)
})
it('should respond with a 400 status code if receive an empty body', async () => {
await request(app).post('/user/signup').send({}).expect(400)
})
})

View file

@ -1,37 +0,0 @@
import app from '../../app'
import request from 'supertest'
import { faker } from '@faker-js/faker'
import type User from '../../interfaces/user'
async function signUpNewUser (): Promise<User> {
// To avoid conflicts with existing usernames or emails
const username = faker.internet.userName({ lastName: 'doe' }).toLowerCase()
const email = faker.internet.email()
const password = faker.internet.password() + '@1'
await request(app)
.post('/user/signup')
.send({
username,
email,
password
})
.expect(200)
const response = await request(app)
.post('/user/auth')
.send({
email,
password
})
.expect(200)
return {
username,
email,
password,
token: response.body.token
}
}
export default signUpNewUser

View file

@ -1,17 +0,0 @@
import prisma from '../../clients/prisma-client'
export default async function deleteUser (username: string) {
await prisma.post.deleteMany({
where: {
author: {
username
}
}
})
await prisma.user.deleteMany({
where: {
username
}
})
}