fixinit10

This commit is contained in:
Hackntosh 2023-06-20 17:05:15 -03:00
commit e214f59fca
30 changed files with 9325 additions and 0 deletions

4
.dockerignore Normal file
View file

@ -0,0 +1,4 @@
.env
node_modules/
.vscode/
client/

5
.editorconfig Normal file
View file

@ -0,0 +1,5 @@
[*.js]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2

17
.env.example Normal file
View file

@ -0,0 +1,17 @@
# Postgres config
POSTGRES_DB=<placeholder>
POSTGRES_USER=<placeholder>
POSTGRES_PASSWORD=<placeholder>
# POSTGRES CONTAINER NAME
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=
# Express
SERVER_PORT=<placeholder>
# Security
JWT_ACCESS_SECRET=<placeholder>

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.git
node_modules
.env
prisma/*.db
.DS_Store
prisma/migrations/dev

15
Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM node:16 as builder
# Create app dir
WORKDIR /app
COPY package*.json ./
COPY prisma ./prisma/
RUN npm install
COPY . .
EXPOSE 8080
CMD ["npm", "run", "prod:start"]

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 CookieDasora
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
README.md Normal file
View file

@ -0,0 +1 @@
# Social Media - Unnamed

17
docker-compose.db.yml Normal file
View file

@ -0,0 +1,17 @@
version: '3.8'
services:
postgres:
image: postgres:alpine
restart: unless-stopped
container_name: postgres
ports:
- 5432:5432
env_file:
- .env
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
name: backend-db

30
docker-compose.yml Normal file
View file

@ -0,0 +1,30 @@
version: '3.8'
services:
api:
container_name: api
restart: unless-stopped
build:
context: .
dockerfile: Dockerfile
ports:
- 8080:8080
depends_on:
- postgres
env_file:
- .env
postgres:
image: postgres:alpine
restart: unless-stopped
container_name: postgres
ports:
- 5432:5432
env_file:
- .env
volumes:
- postgres:/var/lib/postgresql/data
volumes:
postgres:
name: backend-db

8716
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "social-media-app",
"version": "0.0.1",
"description": "A social media",
"main": "src/server.js",
"type": "module",
"scripts": {
"dev:start": "nodemon src/server.js",
"docker": "docker compose up -d",
"docker:build": "docker build -t api . && docker compose up -d",
"docker:db": "docker compose -f docker-compose.db.yml up -d",
"docker:seed": "docker exec -it api npm run prisma:seed",
"migrate:dev": "prisma migrate dev",
"migrate:dev:create": "prisma migrate dev --create-only",
"migrate:reset": "prisma migrate reset",
"prisma:generate": "npx prisma generate",
"prisma:seed": "prisma db seed",
"prisma:studio": "npx prisma studio",
"prod:start": "pm2-runtime start src/server.js",
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js"
},
"standard": {
"ignore": [
"/src/tests/*.spec.js"
]
},
"author": "Cookie",
"license": "MIT",
"devDependencies": {
"jest": "^29.5.0",
"nodemon": "^2.0.22",
"pm2": "^5.3.0",
"prisma": "^4.16.0",
"standard": "^17.1.0",
"supertest": "^6.3.3"
},
"dependencies": {
"@prisma/client": "^4.16.0",
"bcrypt": "^5.1.0",
"compression": "^1.7.4",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"validator": "^13.9.0"
}
}

11
prisma/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node:14
RUN openssl version -v
RUN uname -a
WORKDIR /app
RUN npm install -g prisma --unsafe-perm
ADD . ./prisma/
CMD ["prisma", "migrate", "up"]

5
prisma/client.js Normal file
View file

@ -0,0 +1,5 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
export default prisma

View file

@ -0,0 +1,17 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"displayName" TEXT,
"username" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

20
prisma/schema.prisma Normal file
View file

@ -0,0 +1,20 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
displayName String?
username String @unique
email String @unique
password String
createdAt DateTime @default(now())
}

28
prisma/seed.js Normal file
View file

@ -0,0 +1,28 @@
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main () {
await prisma.user.deleteMany({
where: {
username: 'cookie_'
}
})
console.log('Seeding database...')
await prisma.user.create({
data: {
displayName: 'Cookie_',
username: 'cookie_',
email: 'cookie@cookie.com',
password: '$2a$10$vBx5LkpZNMC3j1bHopUcp.ZcsMt5xfWlUJJGYIb4511aNccnHmMqi' // cookie
}
})
}
main()
.catch((e) => console.error(e))
.finally(async () => {
await prisma.$disconnect()
})

20
src/app.js Normal file
View file

@ -0,0 +1,20 @@
import 'dotenv/config'
import express from 'express'
import router from './routes.js'
import compression from 'compression'
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(router)
app.use(compression({ level: 9 }))
app.use((_req, res) => {
res.status(404).json({
error: 'Not found'
})
})
export default app

View file

@ -0,0 +1,17 @@
import userAuthService from '../services/user-auth.js'
async function userAuthController (req, res) {
const { email, password } = req.body
const result = await userAuthService({ email, password })
if (result instanceof Error) {
return res.status(400).json({
error: result.message
})
}
res.json(result)
}
export default userAuthController

View file

@ -0,0 +1,15 @@
import userInfoService from '../services/user-info.js'
async function userInfoController (req, res) {
const result = await userInfoService(req)
if (result instanceof Error) {
return res.status(400).json({
error: result.message
})
}
res.json(result)
}
export default userInfoController

View file

@ -0,0 +1,17 @@
import userSignupService from '../services/user-signup.js'
async function userSignupController (req, res) {
const { username, email, password } = req.body
const result = await userSignupService({ username, email, password })
if (result instanceof Error) {
return res.status(400).json({
error: result.message
})
}
return res.json(result)
}
export default userSignupController

View file

@ -0,0 +1,38 @@
import jsonwebtoken from 'jsonwebtoken'
import prisma from '../../prisma/client.js'
function ensureAuthenticated (req, res, next) {
if (!req.headers.authorization || req.headers.authorization.length === 0) {
return res.status(401).json({
error: 'Missing token'
})
}
const token = req.headers.authorization.split(' ')[1]
jsonwebtoken.verify(token, process.env.JWT_ACCESS_SECRET, async (err, decoded) => {
if (err || !decoded) {
return res.status(401).json({
error: err.message
})
}
const user = await prisma.user.findFirst({
where: {
id: decoded.id
}
})
if (!user) {
return res.status(401).json({
error: 'Invalid user'
})
}
req.user = decoded
return next()
})
}
export default ensureAuthenticated

14
src/routes.js Normal file
View file

@ -0,0 +1,14 @@
import { Router } from 'express'
import userSignupController from './controllers/user-signup.js'
import userAuthController from './controllers/user-auth.js'
import userInfoController from './controllers/user-info.js'
import ensureAuthenticated from './middlewares/ensure-authenticated.js'
const router = Router()
router.post('/user/create', userSignupController)
router.post('/user/auth', userAuthController)
router.get('/user/info', ensureAuthenticated, userInfoController)
export default router

5
src/server.js Normal file
View file

@ -0,0 +1,5 @@
import app from './app.js'
app.listen(process.env.SERVER_PORT, () => {
console.log(`Server is running @ ${process.env.SERVER_PORT}`)
})

35
src/services/user-auth.js Normal file
View file

@ -0,0 +1,35 @@
import * as bcrypt from 'bcrypt'
import jsonwebtoken from 'jsonwebtoken'
import prisma from '../../prisma/client.js'
async function userAuthService ({ email, password }) {
const user = await prisma.user.findFirst({
where: {
email
}
})
if (!user) {
return new Error('User does not exists')
}
if (email === undefined || password === undefined) {
return new Error('Missing fields')
}
const validPassword = await bcrypt.compare(password, user.password)
if (validPassword === false) {
return new Error('Invalid email or password')
}
const { id } = user
const bearer = jsonwebtoken.sign({ id }, process.env.JWT_ACCESS_SECRET, { expiresIn: '1d' })
return {
token: bearer
}
}
export default userAuthService

21
src/services/user-info.js Normal file
View file

@ -0,0 +1,21 @@
import prisma from '../../prisma/client.js'
async function userInfoService (req) {
const userId = req.user.id
const user = await prisma.user.findFirst({
where: {
id: userId
},
select: {
id: true,
displayName: true,
username: true,
createdAt: true
}
})
return user
}
export default userInfoService

View file

@ -0,0 +1,40 @@
import * as bcrypt from 'bcrypt'
import prisma from '../../prisma/client.js'
async function userSignupService ({ username, email, password }) {
if (!username || !email || !password) {
return new Error('Missing fields')
}
if (/^[a-zA-Z0-9_]{5,15}$/.test(username) === false) {
return new Error('Username not allowed. Only alphanumerics characters (uppercase and lowercase words), underscore and it must be between 5 and 15 characters')
}
if (await prisma.user.findFirst({ where: { username } })) {
return new Error('Username already in use')
}
if (await prisma.user.findFirst({ where: { email } })) {
return new Error('Email already in use')
}
const salt = await bcrypt.genSalt(15)
const hashedPassword = await bcrypt.hash(password, salt)
const user = await prisma.user.create({
data: {
username,
email,
password: hashedPassword
},
select: {
id: true,
username: true,
createdAt: true
}
})
return user
}
export default userSignupService

View file

@ -0,0 +1,51 @@
import request from 'supertest'
import prisma from '../../prisma/client.js'
import app from '../app.js'
describe('POST /user/auth', () => {
beforeAll(async () => {
await prisma.user.create({
data: {
username: 'test',
email: 'test@test.com',
password: 'pass'
}
})
})
afterAll(async () => {
await prisma.user.deleteMany({
where: {
username: 'test'
}
})
await prisma.$disconnect()
})
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('User does not exists')
})
it('should respond with a error if receive an invalid email or password', async () => {
const response = await request(app)
.post('/user/auth').send({
email: 'test@test.com',
password: 'haha'
}).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

@ -0,0 +1,7 @@
import request from 'supertest'
describe('POST /user/info', () => {
it('TODO', async () => {
})
})

View file

@ -0,0 +1,83 @@
import request from 'supertest'
import prisma from '../../prisma/client.js'
import app from '../app.js'
const mockUser = {
username: 'username11',
email: 'random@email.com',
password: 'totallysafepass'
}
describe('POST /user/create', () => {
it('should respond with a 200 status code', async () => {
const response = await request(app).post('/user/create').send(mockUser).expect(200)
expect(response.body).toHaveProperty('id')
expect(response.body).toHaveProperty('username')
expect(response.body).toHaveProperty('createdAt')
})
it('should respond with a 400 status code if sent any invalid data', async () => {
await request(app).post('/user/create').send({
username: 'username12@',
email: mockUser.email,
password: mockUser.password
}).expect(400)
})
it('should respond with a 400 status code for an existing username', async () => {
await prisma.user.create({
data: {
username: 'username12',
email: 'user@email.com',
password: 'reallystrongpass'
}
})
const response = await request(app).post('/user/create').send({
username: 'username12',
email: 'user1@email.com',
password: 'reallystrongpass'
}).expect(400)
expect(response.body).toHaveProperty('error')
})
it('should respond with a 400 status code for an existing email', async () => {
await prisma.user.create({
data: {
username: 'username13',
email: 'user13@email.com',
password: '1234'
}
})
const response = await request(app).post('/user/create').send({
username: 'heythatscool',
email: 'user13@email.com',
password: '12345'
}).expect(400)
expect(response.body).toHaveProperty('error')
})
it('should respond with a 400 status code if receive an empty body', async () => {
const response = await request(app).post('/user/create').send({}).expect(400)
expect(response.body).toHaveProperty('error')
})
})
afterAll(async () => {
const usersToDelete = ['username11', 'username12', 'username13']
await prisma.user.deleteMany({
where: {
username: {
in: usersToDelete
}
}
})
await prisma.$disconnect()
})