mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 17:41:15 +00:00
Merge pull request #60 from hknsh/new-fixes
feat: nestjs migration, new features
This commit is contained in:
commit
74f411d7b5
161 changed files with 10364 additions and 11758 deletions
3
.commitlintrc.json
Normal file
3
.commitlintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["@commitlint/config-conventional"]
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
.env
|
||||
node_modules/
|
||||
.vscode/
|
||||
client/
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
npm-debug.log
|
||||
dist
|
||||
.env.example
|
||||
package-lock_copy.json
|
||||
package_copy.json
|
||||
docker.env.example
|
19
.env.example
19
.env.example
|
@ -14,20 +14,19 @@ DB_HOST=localhost
|
|||
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=<placeholder>
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=127.0.0.1 # localhost (for docker use redis)
|
||||
REDIS_HOST=127.0.0.1 # localhost (for docker use `redis`)
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=<placehoder>
|
||||
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||
|
||||
# Express
|
||||
# Fastify
|
||||
SERVER_PORT=<placeholder>
|
||||
CLIENT_URL=<placeholder>
|
||||
SERVER_HOST=<placeholder>
|
||||
|
||||
# Security
|
||||
JWT_ACCESS_SECRET=<placeholder>
|
||||
|
||||
# Localstack - The data can be fake (Change this to real data on production)
|
||||
SERVICES=s3
|
||||
AWS_ACCESS_KEY_ID=<placeholder>
|
||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_DEFAULT_OUTPUT=json
|
||||
# Minio
|
||||
MINIO_ROOT_USER=<username>
|
||||
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||
MINIO_ENDPOINT=<url>
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
node_modules
|
||||
dist
|
|
@ -1,17 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["plugin:prettier/recommended"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": 1
|
||||
}
|
||||
}
|
18
.github/workflows/pull_request.yml
vendored
Normal file
18
.github/workflows/pull_request.yml
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
name: Biome
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
with:
|
||||
version: latest
|
||||
- name: Run Biome
|
||||
run: biome check --apply .
|
45
.gitignore
vendored
45
.gitignore
vendored
|
@ -1,11 +1,44 @@
|
|||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
old_port
|
||||
.git
|
||||
node_modules
|
||||
.env
|
||||
prisma/*.db
|
||||
.DS_Store
|
||||
prisma/migrations/dev
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
package_backup.json
|
||||
logs/
|
||||
docker.env
|
||||
package_copy.json
|
||||
package-lock_copy.json
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx --no -- commitlint --edit
|
||||
|
|
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid",
|
||||
"useTabs": false,
|
||||
"endOfLine": "lf",
|
||||
"tabWidth": 2
|
||||
}
|
22
.swcrc
22
.swcrc
|
@ -1,26 +1,14 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"sourceMaps": true,
|
||||
"jsc": {
|
||||
"target": "esnext",
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": false,
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"target": "es2020",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"clients/*": ["clients/*"],
|
||||
"config/*": ["config/*"],
|
||||
"controllers/*": ["controllers/*"],
|
||||
"interfaces/*": ["interfaces/*"],
|
||||
"helpers/*": ["helpers/*"],
|
||||
"middlewares/*": ["middlewares/*"],
|
||||
"services/*": ["services/*"]
|
||||
}
|
||||
"baseUrl": "./"
|
||||
},
|
||||
"exclude": ["@types/", "interfaces/"],
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
"minify": true
|
||||
"minify": false
|
||||
}
|
||||
|
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["biomejs.biome"]
|
||||
}
|
72
Dockerfile
72
Dockerfile
|
@ -1,34 +1,50 @@
|
|||
FROM node:18 as builder
|
||||
# LOCAL
|
||||
FROM node:20-alpine AS dev
|
||||
|
||||
# Create app dir
|
||||
WORKDIR /app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY package*.json ./
|
||||
COPY prisma ./prisma/
|
||||
|
||||
RUN npm install -D @swc/cli @swc/core
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Stage 2
|
||||
FROM node:18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm i pm2 -g
|
||||
|
||||
COPY --from=builder /app/package*.json ./
|
||||
COPY --from=builder /app/prisma ./prisma/
|
||||
COPY --from=builder /app/dist ./dist/
|
||||
COPY --from=builder /app/docker.env ./
|
||||
|
||||
RUN mv docker.env .env
|
||||
COPY --chown=node:node package*.json ./
|
||||
COPY --chown=node:node prisma ./prisma/
|
||||
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 8080
|
||||
COPY --chown=node:node . .
|
||||
|
||||
CMD ["npm", "run", "prod:start"]
|
||||
USER node
|
||||
|
||||
# BUILD
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY --chown=node:node package*.json ./
|
||||
COPY --chown=node:node --from=dev /usr/src/app/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=dev /usr/src/app/prisma ./prisma/
|
||||
COPY --chown=node:node .husky ./.husky
|
||||
COPY --chown=node:node tsconfig.json tsconfig.build.json ./
|
||||
COPY --chown=node:node docker.env ./.env
|
||||
COPY --chown=node:node . .
|
||||
|
||||
RUN npm install husky -g
|
||||
|
||||
RUN npm run prisma:generate
|
||||
|
||||
RUN npm run build
|
||||
|
||||
ENV NODE_ENV production
|
||||
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
USER node
|
||||
|
||||
# PROD
|
||||
FROM node:20-alpine as production
|
||||
|
||||
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
|
||||
COPY --chown=node:node --from=build /usr/src/app/dist ./dist
|
||||
COPY --chown=node:node --from=build /usr/src/app/prisma ./prisma
|
||||
COPY --chown=node:node --from=build /usr/src/app/.husky ./.husky
|
||||
COPY --chown=node:node --from=build /usr/src/app/.env ./
|
||||
COPY --chown=node:node --from=build /usr/src/app/package*.json ./
|
||||
|
||||
CMD ["npm", "run" , "prod"]
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 CookieDasora
|
||||
Copyright (c) 2024 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
|
||||
|
|
83
README.md
83
README.md
|
@ -1,30 +1,73 @@
|
|||
<p align="center">
|
||||
<img src="./banner.svg" alt="LocalStack - A fully functional local cloud stack">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./resources/logo-light.svg">
|
||||
<source media="(prefers-color-scheme: light)" srcset="./resources/logo-dark.svg">
|
||||
<img alt="Project Knedita" src="./resources/logo-light.svg" width="700">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
## Stack
|
||||
A simple RESTful API made with **NestJS** and **Fastify**.
|
||||
|
||||
**Client**: NextJS, TailwindCSS and Radix UI Icons.
|
||||
You can find the front-end [here](https://github.com/CookieDasora/project-knedita-client)
|
||||
### 🚀 Preparing the environment
|
||||
|
||||
**Server**: ExpressJS, Jest, Docker, Postgresql, Redis, Prisma, AWS, SWC and Typescript
|
||||
Make sure that you have Node, NPM, Docker and Docker Compose installed on your computer.
|
||||
|
||||
## To-do - Backend
|
||||
First, install the necessary packages with the following commands:
|
||||
|
||||
- Create/update/delete Posts ✅
|
||||
- Add post attachments
|
||||
- Create/update/delete Users ✅
|
||||
- Password recuperation
|
||||
- Two step verification
|
||||
- Able to choose a profile picture✅
|
||||
- Probably gonna use LocalStack to mock Amazon S3✅
|
||||
- Image compression ✅
|
||||
- Following/unfollowing features ✅
|
||||
- Like posts ✅
|
||||
- Authentication ✅
|
||||
- Add more verification (like, if the password is too short) ✅
|
||||
- Set display name ✅
|
||||
- Add rate limit ✅
|
||||
```bash
|
||||
$ npm i
|
||||
```
|
||||
|
||||
After that, you can update the `.env` and the `docker.env` files. The `.env` file is for development environment and the `docker.env` is for production.
|
||||
|
||||
You can find the templates for those files on `.env.example` and `docker.env.example`.
|
||||
|
||||
To run the necessary services you can execute the following command:
|
||||
|
||||
```bash
|
||||
$ npm run docker:db
|
||||
```
|
||||
|
||||
This will start the following services:
|
||||
- **PostgreSQL**
|
||||
- **Redis**
|
||||
- **MinIO**
|
||||
|
||||
Apply the migrations to the database with the following command:
|
||||
|
||||
```bash
|
||||
$ npm run migrate:dev
|
||||
```
|
||||
|
||||
And now, you can start the server with the command:
|
||||
|
||||
```bash
|
||||
$ npm run dev:start
|
||||
```
|
||||
|
||||
You can check the documentation accessing the endpoint `/` in your browser
|
||||
|
||||
To run in production you can use the following command:
|
||||
|
||||
```bash
|
||||
$ npm run docker
|
||||
```
|
||||
|
||||
This will start all the previous services and the back-end image.
|
||||
|
||||
## 🗄️ Stack
|
||||
|
||||
This back-end uses the following stack:
|
||||
- **Docker**
|
||||
- **Fastify**
|
||||
- **MinIO**
|
||||
- **NestJS**
|
||||
- **Passport**
|
||||
- **PostgreSQL**
|
||||
- **Prisma**
|
||||
- **Redis**
|
||||
- **Swagger**
|
||||
- **Typescript**
|
||||
|
||||
## License
|
||||
|
||||
|
|
20
biome.json
Normal file
20
biome.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["dist/**", "node_modules"]
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
}
|
||||
extends: ["@commitlint/config-conventional"],
|
||||
};
|
||||
|
|
|
@ -4,6 +4,9 @@ networks:
|
|||
localstack-net:
|
||||
name: localstack-net
|
||||
driver: bridge
|
||||
minio_network:
|
||||
name: minio_network
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
@ -27,24 +30,23 @@ services:
|
|||
volumes:
|
||||
- redis:/data
|
||||
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack_main
|
||||
minio:
|
||||
image: bitnami/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- localstack-net
|
||||
ports:
|
||||
- 4566:4566
|
||||
- 4572:4572
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
networks:
|
||||
- minio_network
|
||||
volumes:
|
||||
- 'minio_data:/data'
|
||||
env_file:
|
||||
- docker.env
|
||||
volumes:
|
||||
- localstack:/data
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
name: backend-db
|
||||
redis:
|
||||
driver: local
|
||||
localstack:
|
||||
minio_data:
|
||||
driver: local
|
||||
|
|
|
@ -4,23 +4,11 @@ networks:
|
|||
localstack-net:
|
||||
name: localstack-net
|
||||
driver: bridge
|
||||
minio_network:
|
||||
name: minio_network
|
||||
driver: bridge
|
||||
|
||||
services:
|
||||
api:
|
||||
container_name: api
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 8080:8080
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- localstack
|
||||
env_file:
|
||||
- docker.env
|
||||
|
||||
postgres:
|
||||
image: postgres:alpine
|
||||
restart: unless-stopped
|
||||
|
@ -32,20 +20,18 @@ services:
|
|||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
|
||||
localstack:
|
||||
image: localstack/localstack
|
||||
container_name: localstack_main
|
||||
minio:
|
||||
image: bitnami/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- localstack-net
|
||||
ports:
|
||||
- 4566:4566
|
||||
- 4572:4572
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
networks:
|
||||
- minio_network
|
||||
volumes:
|
||||
- 'minio_data:/data'
|
||||
env_file:
|
||||
- docker.env
|
||||
volumes:
|
||||
- localstack:/data
|
||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
|
@ -57,9 +43,25 @@ services:
|
|||
volumes:
|
||||
- redis:/data
|
||||
|
||||
api:
|
||||
container_name: api
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 3000:3000
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
- minio
|
||||
env_file:
|
||||
- docker.env
|
||||
|
||||
volumes:
|
||||
postgres:
|
||||
name: backend-db
|
||||
redis:
|
||||
driver: local
|
||||
localstack:
|
||||
minio_data:
|
||||
driver: local
|
||||
|
|
|
@ -5,26 +5,27 @@ NODE_ENV=production
|
|||
POSTGRES_DB=<placeholder>
|
||||
POSTGRES_USER=<placeholder>
|
||||
POSTGRES_PASSWORD=<placeholder>
|
||||
# Use this hostname on Docker
|
||||
DB_HOST=postgres
|
||||
|
||||
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:<placeholder>/${POSTGRES_DB}?schema=<placeholder>
|
||||
DATABASE_URL=postgresql://<USER>:<PASS>@${DB_HOST}:<PORT>/${POSTGRES_DB}?schema=<PROD-SCHEMA>
|
||||
|
||||
# Redis
|
||||
# Use this hostname on Docker
|
||||
REDIS_HOST=redis
|
||||
|
||||
REDIS_PORT=<placeholder>
|
||||
REDIS_PASSWORD=<placeholder>
|
||||
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||
|
||||
# Express
|
||||
# Fastify
|
||||
SERVER_PORT=<placeholder>
|
||||
SERVER_HOST=<placeholder>
|
||||
|
||||
# Security
|
||||
JWT_ACCESS_SECRET=<placeholder>
|
||||
|
||||
# Localstack - The data can be fake
|
||||
SERVICES=s3
|
||||
AWS_ACCESS_KEY_ID=<placeholder>
|
||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_DEFAULT_OUTPUT=json
|
||||
AWS_BUCKET=<placeholder>
|
||||
# Minio
|
||||
MINIO_ROOT_USER=<username>
|
||||
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||
MINIO_ENDPOINT=<url>
|
||||
|
|
10
nest-cli.json
Normal file
10
nest-cli.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"builder": "swc",
|
||||
"typeCheck": true
|
||||
}
|
||||
}
|
14963
package-lock.json
generated
14963
package-lock.json
generated
File diff suppressed because it is too large
Load diff
180
package.json
180
package.json
|
@ -1,94 +1,116 @@
|
|||
{
|
||||
"name": "social-media-app",
|
||||
"version": "0.0.1",
|
||||
"description": "A social media",
|
||||
"name": "project-knedita",
|
||||
"version": "0.1.0",
|
||||
"description": "A open-source social media",
|
||||
"author": "hknsh",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "swc src -d dist",
|
||||
"dev:start": "ts-node-dev -r tsconfig-paths/register --transpile-only --respawn src/server.ts",
|
||||
"build": "nest build",
|
||||
"dev:start": "nest start --watch",
|
||||
"dev:debug": "nest start --debug --watch",
|
||||
"docker": "docker compose --env-file docker.env 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",
|
||||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts .",
|
||||
"lint": "npx @biomejs/biome check --apply .",
|
||||
"migrate:deploy": "prisma migrate deploy",
|
||||
"migrate:dev": "prisma migrate dev",
|
||||
"migrate:dev:create": "prisma migrate dev --create-only",
|
||||
"migrate:reset": "prisma migrate reset",
|
||||
"prepare": "husky install",
|
||||
"prisma:generate": "npx prisma generate",
|
||||
"prisma:seed": "prisma db seed",
|
||||
"prisma:studio": "npx prisma studio",
|
||||
"prod:start": "npx prisma migrate deploy && pm2-runtime start dist/server.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"ts-standard": {
|
||||
"project": "tsconfig.json",
|
||||
"ignore": [
|
||||
"prisma/*",
|
||||
"dist"
|
||||
]
|
||||
},
|
||||
"author": "Cookie",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.7.2",
|
||||
"@commitlint/config-conventional": "^17.7.0",
|
||||
"@faker-js/faker": "^8.4.0",
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/dotenv": "^8.2.0",
|
||||
"@types/express": "^4.17.19",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/multer-s3": "^3.0.0",
|
||||
"@types/node": "^20.11.5",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"@types/swagger-ui-express": "^4.1.4",
|
||||
"@types/validator": "^13.7.17",
|
||||
"@typescript-eslint/eslint-plugin": "^6.8.0",
|
||||
"@typescript-eslint/parser": "^6.7.5",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard-with-typescript": "^43.0.0",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"husky": "^8.0.3",
|
||||
"nodemon": "^3.0.1",
|
||||
"pm2": "^5.3.1",
|
||||
"prettier": "^3.2.4",
|
||||
"prisma": "^5.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite-tsconfig-paths": "^4.3.1",
|
||||
"vitest": "^0.34.6"
|
||||
"prepare": "husky",
|
||||
"prod": "npm run migrate:deploy && node dist/main",
|
||||
"start": "nest start",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.4.1",
|
||||
"aws-sdk": "^2.1545.0",
|
||||
"bcrypt": "^5.1.0",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.2",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.1.1",
|
||||
"@aws-sdk/client-s3": "^3.502.0",
|
||||
"@fastify/helmet": "^11.1.1",
|
||||
"@fastify/multipart": "^8.1.0",
|
||||
"@fastify/static": "^6.12.0",
|
||||
"@nest-lab/fastify-multer": "^1.2.0",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/platform-fastify": "^10.3.1",
|
||||
"@nestjs/swagger": "^7.2.0",
|
||||
"@nestjs/throttler": "^5.1.1",
|
||||
"@prisma/client": "^5.9.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"file-type": "^19.0.0",
|
||||
"ioredis": "^5.3.2",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer-s3": "^3.0.1",
|
||||
"rate-limit-redis": "^4.0.0",
|
||||
"redis": "^4.6.7",
|
||||
"sharp": "^0.32.3",
|
||||
"socket.io": "^4.7.2",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"validator": "^13.9.0",
|
||||
"winston": "^3.11.0",
|
||||
"yaml": "^2.3.4"
|
||||
"nestjs-s3": "^2.0.1",
|
||||
"nestjs-throttler-storage-redis": "^0.4.1",
|
||||
"nestjs-zod": "^3.0.0",
|
||||
"passport": "^0.7.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.5.3",
|
||||
"@commitlint/cli": "^18.6.0",
|
||||
"@commitlint/config-conventional": "^18.6.0",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@swc/cli": "^0.1.65",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@swc/jest": "^0.2.31",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/passport-jwt": "^4.0.0",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"husky": "^9.0.7",
|
||||
"jest": "^29.5.0",
|
||||
"lint-staged": "^15.2.1",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.9.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": [
|
||||
"@swc/jest"
|
||||
]
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"lint-staged": {
|
||||
"**/*.@(js|ts|json)": "biome check --apply --no-errors-on-unmatched"
|
||||
}
|
||||
}
|
||||
|
|
63
prisma/migrations/20240126133535_renamed_posts/migration.sql
Normal file
63
prisma/migrations/20240126133535_renamed_posts/migration.sql
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `postId` on the `Comments` table. All the data in the column will be lost.
|
||||
- You are about to drop the `Post` table. If the table is not empty, all the data it contains will be lost.
|
||||
- You are about to drop the `PostLike` table. If the table is not empty, all the data it contains will be lost.
|
||||
- Added the required column `kweekId` to the `Comments` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Comments" DROP CONSTRAINT "Comments_postId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "Post" DROP CONSTRAINT "Post_authorId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PostLike" DROP CONSTRAINT "PostLike_postId_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "PostLike" DROP CONSTRAINT "PostLike_userId_fkey";
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Comments" DROP COLUMN "postId",
|
||||
ADD COLUMN "kweekId" TEXT NOT NULL;
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "Post";
|
||||
|
||||
-- DropTable
|
||||
DROP TABLE "PostLike";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Kweek" (
|
||||
"id" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"authorId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Kweek_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "KweekLike" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kweekId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "KweekLike_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Kweek" ADD CONSTRAINT "Kweek_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "KweekLike" ADD CONSTRAINT "KweekLike_kweekId_fkey" FOREIGN KEY ("kweekId") REFERENCES "Kweek"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "KweekLike" ADD CONSTRAINT "KweekLike_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Comments" ADD CONSTRAINT "Comments_kweekId_fkey" FOREIGN KEY ("kweekId") REFERENCES "Kweek"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Kweek" ADD COLUMN "attachments" TEXT[];
|
|
@ -13,34 +13,35 @@ model User {
|
|||
username String @unique
|
||||
email String @unique
|
||||
password String
|
||||
posts Post[]
|
||||
kweeks Kweek[]
|
||||
profileImage String?
|
||||
likedPosts PostLike[]
|
||||
likedKweeks KweekLike[]
|
||||
likedComments CommentLike[]
|
||||
followers Follows[] @relation("following")
|
||||
following Follows[] @relation("follower")
|
||||
postComments Comments[]
|
||||
followers Follows[] @relation("follower")
|
||||
following Follows[] @relation("following")
|
||||
kweeksComments Comments[]
|
||||
fromNotifications Notifications[] @relation("fromNotifications")
|
||||
toNotifications Notifications[] @relation("toNotifications")
|
||||
socketId String?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Post {
|
||||
model Kweek {
|
||||
id String @id @default(uuid())
|
||||
content String
|
||||
authorId String
|
||||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||
likes PostLike[]
|
||||
likes KweekLike[]
|
||||
comments Comments[]
|
||||
attachments String[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model PostLike {
|
||||
model KweekLike {
|
||||
id String @id @default(uuid())
|
||||
postId String
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
kweekId String
|
||||
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
|
@ -71,8 +72,8 @@ model Comments {
|
|||
content String
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
postId String
|
||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
||||
kweekId String
|
||||
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||
likes CommentLike[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt @default(now())
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
5
resources/logo-dark.svg
Normal file
5
resources/logo-dark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 36 KiB |
5
resources/logo-light.svg
Normal file
5
resources/logo-light.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 36 KiB |
13
src/@types/express.d.ts
vendored
13
src/@types/express.d.ts
vendored
|
@ -1,13 +0,0 @@
|
|||
/* eslint-disable */
|
||||
import * as express from 'express'
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
namespace Multer {
|
||||
interface File {
|
||||
location: string
|
||||
key: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { expect, describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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')
|
||||
})
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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)
|
||||
})
|
||||
})
|
|
@ -1,55 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { expect, describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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')
|
||||
})
|
||||
})
|
|
@ -1,57 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { expect, describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
|
@ -1,46 +0,0 @@
|
|||
import app from '../../app'
|
||||
import deleteUser from '../utils/delete-user'
|
||||
import { expect, describe, beforeAll, afterAll, it } from 'vitest'
|
||||
import request from 'supertest'
|
||||
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')
|
||||
})
|
||||
})
|
|
@ -1,20 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { describe, beforeAll, it } from 'vitest'
|
||||
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)
|
||||
})
|
||||
})
|
|
@ -1,38 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { expect, describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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')
|
||||
expect(response.body).toHaveProperty('likedPosts')
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
|
@ -1,46 +0,0 @@
|
|||
import app from '../../app'
|
||||
import { describe, beforeAll, afterAll, it } from 'vitest'
|
||||
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/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)
|
||||
})
|
||||
})
|
|
@ -1,37 +0,0 @@
|
|||
import app from '../../app'
|
||||
import request from 'supertest'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import type userPayload from '../../interfaces/user'
|
||||
|
||||
async function signUpNewUser(): Promise<userPayload> {
|
||||
// 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
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
56
src/app.module.ts
Normal file
56
src/app.module.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { S3Module } from "nestjs-s3";
|
||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
||||
import { ZodValidationPipe } from "nestjs-zod";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||
import { KweeksModule } from "./kweeks/kweeks.module";
|
||||
import { UserModule } from "./users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
AuthModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
ThrottlerModule.forRoot({
|
||||
throttlers: [{ limit: 10, ttl: 60000 }],
|
||||
storage: new ThrottlerStorageRedisService(
|
||||
`redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
|
||||
),
|
||||
}),
|
||||
KweeksModule,
|
||||
FastifyMulterModule,
|
||||
S3Module.forRoot({
|
||||
config: {
|
||||
credentials: {
|
||||
accessKeyId: process.env.MINIO_ROOT_USER, // CHANGE WHEN PRODUCTION TO S3
|
||||
secretAccessKey: process.env.MINIO_ROOT_PASSWORD,
|
||||
},
|
||||
region: "us-east-1",
|
||||
endpoint: process.env.MINIO_ENDPOINT,
|
||||
forcePathStyle: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_PIPE,
|
||||
useClass: ZodValidationPipe,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottlerGuard,
|
||||
},
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: JwtAuthGuard,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
49
src/app.ts
49
src/app.ts
|
@ -1,49 +0,0 @@
|
|||
import 'dotenv/config'
|
||||
|
||||
import compression from 'compression'
|
||||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import limiter from 'middlewares/rate-limit'
|
||||
import morganMiddleware from 'middlewares/morgan'
|
||||
import router from './routes'
|
||||
import swaggerUI from 'swagger-ui-express'
|
||||
import swaggerDocument from 'helpers/parse-swagger'
|
||||
import swaggerConfig from 'config/swagger'
|
||||
|
||||
const app = express()
|
||||
|
||||
// TODO: test socket io, emit notifications when create one.
|
||||
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
app.use(morganMiddleware)
|
||||
app.options('*', cors())
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: process.env.CLIENT_URL,
|
||||
methods: ['GET', 'POST', 'PUT'],
|
||||
optionsSuccessStatus: 200,
|
||||
}),
|
||||
)
|
||||
app.use(express.static('public'))
|
||||
app.use(limiter)
|
||||
app.use(router)
|
||||
app.use(
|
||||
'/docs',
|
||||
swaggerUI.serve,
|
||||
swaggerUI.setup(swaggerDocument, swaggerConfig),
|
||||
)
|
||||
app.use(compression({ level: 9 }))
|
||||
|
||||
app.get('/', function (_req, res) {
|
||||
res.redirect('/docs')
|
||||
})
|
||||
|
||||
app.use((_req, res) => {
|
||||
res.status(404).json({
|
||||
error: 'Endpoint not found',
|
||||
})
|
||||
})
|
||||
|
||||
export default app
|
35
src/auth/auth.controller.ts
Normal file
35
src/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
Post,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { Public } from "src/decorators/public.decorator";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { LoginUserDTO } from "./dto/login.dto";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
|
||||
@ApiTags("Auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Public()
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post("/")
|
||||
@ApiOperation({ summary: "Authenticates a user" })
|
||||
@ApiOkResponse({ status: 200, description: "Authenticated successfully" })
|
||||
@ApiUnauthorizedResponse({ description: "Wrong username or password" })
|
||||
@HttpCode(200)
|
||||
async login(@Request() req, @Body() _: LoginUserDTO) {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
}
|
22
src/auth/auth.module.ts
Normal file
22
src/auth/auth.module.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { UserModule } from "src/users/users.module";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
imports: [
|
||||
UserModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_ACCESS_SECRET,
|
||||
signOptions: { expiresIn: "1d" }, // TODO: add refresh tokens
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
})
|
||||
export class AuthModule {}
|
46
src/auth/auth.service.ts
Normal file
46
src/auth/auth.service.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import * as bcrypt from "bcrypt";
|
||||
import { UserModel } from "src/users/models/user.model";
|
||||
import { UserService } from "src/users/users.service";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
async validateUser(
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<UserModel | null> {
|
||||
const user = await this.userService.auth_search(username);
|
||||
|
||||
if (user === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const validation = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (user && validation) {
|
||||
const { password, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: UserModel): Promise<{ token: string }> {
|
||||
const payload = {
|
||||
displayName: user.displayName,
|
||||
username: user.username,
|
||||
profileImage: user.profileImage,
|
||||
sub: user.id,
|
||||
};
|
||||
|
||||
return {
|
||||
token: this.jwtService.sign(payload),
|
||||
};
|
||||
}
|
||||
}
|
29
src/auth/dto/login.dto.ts
Normal file
29
src/auth/dto/login.dto.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { createZodDto } from "nestjs-zod";
|
||||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const LoginUserSchema = z
|
||||
.object({
|
||||
username: z
|
||||
.string({
|
||||
required_error: "Username is required",
|
||||
})
|
||||
.regex(
|
||||
/^[a-zA-Z0-9_.]{5,15}$/,
|
||||
"The username must have alphanumerics characters, underscore, dots and it must be between 5 and 15 characters",
|
||||
)
|
||||
.toLowerCase(),
|
||||
password: z
|
||||
.password({
|
||||
required_error: "Password is required",
|
||||
})
|
||||
.min(8)
|
||||
.max(32)
|
||||
.atLeastOne("digit")
|
||||
.atLeastOne("uppercase")
|
||||
.atLeastOne("lowercase")
|
||||
.atLeastOne("special")
|
||||
.transform((value) => value.replace(/\s+/g, "")), // Removes every whitespace
|
||||
})
|
||||
.required();
|
||||
|
||||
export class LoginUserDTO extends createZodDto(LoginUserSchema) {}
|
25
src/auth/jwt-auth.guard.ts
Normal file
25
src/auth/jwt-auth.guard.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { Observable } from "rxjs";
|
||||
import { IS_PUBLIC_KEY } from "src/decorators/public.decorator";
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
return super.canActivate(context);
|
||||
}
|
||||
}
|
28
src/auth/jwt.strategy.ts
Normal file
28
src/auth/jwt.strategy.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
|
||||
type Payload = {
|
||||
displayName: string;
|
||||
username: string;
|
||||
sub: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor() {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: process.env.JWT_ACCESS_SECRET,
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: Payload) {
|
||||
return {
|
||||
displayName: payload.displayName,
|
||||
username: payload.username,
|
||||
id: payload.sub,
|
||||
};
|
||||
}
|
||||
}
|
5
src/auth/local-auth.guard.ts
Normal file
5
src/auth/local-auth.guard.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
20
src/auth/local.strategy.ts
Normal file
20
src/auth/local.strategy.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy } from "passport-local";
|
||||
import { UserModel } from "src/users/models/user.model";
|
||||
import { AuthService } from "./auth.service";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async validate(username: string, password: string): Promise<UserModel> {
|
||||
const user = await this.authService.validateUser(username, password);
|
||||
if (!user) {
|
||||
throw new UnauthorizedException("Wrong username or password");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
export default prisma
|
|
@ -1,27 +0,0 @@
|
|||
import logger from 'helpers/logger'
|
||||
import { createClient, type RedisClientOptions } from 'redis'
|
||||
|
||||
const redisPort = parseInt(process.env.REDIS_PORT ?? '6379', 10)
|
||||
const redisHost = process.env.REDIS_HOST ?? '127.0.0.1'
|
||||
const redisPassword = process.env.REDIS_PASSWORD ?? ''
|
||||
|
||||
const redisConfig: RedisClientOptions = {
|
||||
url: `redis://:${redisPassword}@${redisHost}:${redisPort}/0`,
|
||||
}
|
||||
|
||||
const redis = createClient(redisConfig)
|
||||
|
||||
redis
|
||||
.connect()
|
||||
.then(() => {
|
||||
logger.info('Successfully connected to Redis')
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
logger.error(`Error while connecting to Redis: ${e.message}`)
|
||||
})
|
||||
|
||||
redis.on('error', async (e: Error) => {
|
||||
logger.error(`Error in Redis client: ${e.message}`)
|
||||
})
|
||||
|
||||
export default redis
|
|
@ -1,24 +0,0 @@
|
|||
import { S3Client } from '@aws-sdk/client-s3'
|
||||
import logger from 'helpers/logger'
|
||||
|
||||
let s3: S3Client
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
logger.info('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
|
|
@ -1,65 +0,0 @@
|
|||
import multer from 'multer'
|
||||
import { type 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.res?.locals.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.res?.locals.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
|
|
@ -1,9 +0,0 @@
|
|||
import type { SwaggerUiOptions } from 'swagger-ui-express'
|
||||
|
||||
const swaggerConfig: SwaggerUiOptions = {
|
||||
customCssUrl: '/swagger-ui.css',
|
||||
customSiteTitle: 'Project Knedita Docs',
|
||||
customfavIcon: '/favicon.png',
|
||||
}
|
||||
|
||||
export default swaggerConfig
|
|
@ -1,22 +0,0 @@
|
|||
import { Router } from 'express'
|
||||
|
||||
// Controllers
|
||||
import comments from './comments'
|
||||
|
||||
// Middlewares
|
||||
import authenticated from 'middlewares/authenticated'
|
||||
|
||||
const commentsRouter = Router()
|
||||
|
||||
// GET
|
||||
commentsRouter.get('/fetch-likes', comments.fetchLikes)
|
||||
commentsRouter.get('/info', comments.fetch)
|
||||
|
||||
// POST
|
||||
commentsRouter.post('/create', authenticated, comments.create)
|
||||
commentsRouter.post('/delete', authenticated, comments.delete)
|
||||
|
||||
// PUT
|
||||
commentsRouter.put('/update', authenticated, comments.update)
|
||||
|
||||
export default commentsRouter
|
|
@ -1,28 +0,0 @@
|
|||
import comment from 'services/comments'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function commentCreateController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { content, postId } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
if (postId === undefined) {
|
||||
badRequest(res, 'Expected post id')
|
||||
return
|
||||
}
|
||||
|
||||
if (content === undefined) {
|
||||
badRequest(res, 'Expected comment content')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await comment.create(postId, content, id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default commentCreateController
|
|
@ -1,23 +0,0 @@
|
|||
import comment from 'services/comments'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function commentDeleteController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { commentId } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
if (commentId === undefined) {
|
||||
badRequest(res, 'Expected comment id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await comment.delete(commentId, id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default commentDeleteController
|
|
@ -1,22 +0,0 @@
|
|||
import comment from 'services/comments'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function commentFetchController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const commentId = req.query.id as string
|
||||
|
||||
if (commentId === undefined) {
|
||||
badRequest(res, 'Expected comment id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await comment.fetch(commentId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default commentFetchController
|
|
@ -1,22 +0,0 @@
|
|||
import comment from 'services/comments'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function commentFetchLikesController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const commentId = req.query.id as string
|
||||
|
||||
if (commentId === undefined) {
|
||||
badRequest(res, 'Expected comment id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await comment.fetchLikes(commentId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default commentFetchLikesController
|
|
@ -1,15 +0,0 @@
|
|||
import commentCreateController from './create'
|
||||
import commentDeleteController from './delete'
|
||||
import commentFetchController from './fetch-info'
|
||||
import commentFetchLikesController from './fetch-likes'
|
||||
import commentUpdateController from './update'
|
||||
|
||||
const comments = {
|
||||
create: commentCreateController,
|
||||
delete: commentDeleteController,
|
||||
fetch: commentFetchController,
|
||||
fetchLikes: commentFetchLikesController,
|
||||
update: commentUpdateController,
|
||||
} as const
|
||||
|
||||
export default comments
|
|
@ -1,28 +0,0 @@
|
|||
import comment from 'services/comments'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function commentUpdateController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { commentId, content } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
if (commentId === undefined) {
|
||||
badRequest(res, 'Expected comment content')
|
||||
return
|
||||
}
|
||||
|
||||
if (content === undefined) {
|
||||
badRequest(res, 'Expected content to update')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await comment.update(content, id, commentId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default commentUpdateController
|
|
@ -1,22 +0,0 @@
|
|||
import { Router } from 'express'
|
||||
|
||||
// Controllers
|
||||
import post from './posts'
|
||||
|
||||
// Middlewares
|
||||
import authenticated from 'middlewares/authenticated'
|
||||
|
||||
const postsRouter = Router()
|
||||
|
||||
// GET
|
||||
postsRouter.get('/fetch-likes', post.fetchLikes)
|
||||
postsRouter.get('/info', post.fetch)
|
||||
|
||||
// POST
|
||||
postsRouter.post('/create', authenticated, post.create)
|
||||
postsRouter.post('/delete', authenticated, post.delete)
|
||||
|
||||
// PUT
|
||||
postsRouter.put('/update', authenticated, post.update)
|
||||
|
||||
export default postsRouter
|
|
@ -1,23 +0,0 @@
|
|||
import post from 'services/posts'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function postCreateController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { content } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
if (content === undefined) {
|
||||
badRequest(res, 'Expected post content')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await post.create(content, id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default postCreateController
|
|
@ -1,23 +0,0 @@
|
|||
import post from 'services/posts'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function postDeleteController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userId = res.locals.user.id
|
||||
const postId = req.body.postId
|
||||
|
||||
if (postId === undefined) {
|
||||
badRequest(res, 'Missing post id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await post.delete(postId, userId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default postDeleteController
|
|
@ -1,22 +0,0 @@
|
|||
import post from 'services/posts'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function postFetchInfoController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = req.query.id as string
|
||||
|
||||
if (id === undefined) {
|
||||
badRequest(res, 'Missing post id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await post.fetch(id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default postFetchInfoController
|
|
@ -1,22 +0,0 @@
|
|||
import post from 'services/posts'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function postFetchLikesController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = req.query.id as string
|
||||
|
||||
if (id === undefined) {
|
||||
badRequest(res, 'Missing post id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await post.fetchLikes(id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default postFetchLikesController
|
|
@ -1,15 +0,0 @@
|
|||
import postCreateController from './create'
|
||||
import postDeleteController from './delete'
|
||||
import postFetchInfoController from './fetch-info'
|
||||
import postUpdateController from './update'
|
||||
import postFetchLikesController from './fetch-likes'
|
||||
|
||||
const post = {
|
||||
create: postCreateController,
|
||||
delete: postDeleteController,
|
||||
fetch: postFetchInfoController,
|
||||
fetchLikes: postFetchLikesController,
|
||||
update: postUpdateController,
|
||||
} as const
|
||||
|
||||
export default post
|
|
@ -1,17 +0,0 @@
|
|||
import post from 'services/posts'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function postUpdateController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { postId, content } = req.body
|
||||
const userId = res.locals.user.id
|
||||
|
||||
const result = await post.update(postId, content, userId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default postUpdateController
|
|
@ -1,37 +0,0 @@
|
|||
import { Router } from 'express'
|
||||
|
||||
// Controllers
|
||||
import user from './users'
|
||||
|
||||
// Middlewares
|
||||
import authenticated from 'middlewares/authenticated'
|
||||
import uploadFile from 'middlewares/upload-image'
|
||||
|
||||
const usersRouter = Router()
|
||||
|
||||
// GET
|
||||
usersRouter.get('/fetch-posts', user.fetchPosts)
|
||||
usersRouter.get('/info', user.fetchInfo)
|
||||
usersRouter.get('/search', user.searchUser)
|
||||
|
||||
// POST
|
||||
usersRouter.post('/auth', user.auth)
|
||||
usersRouter.post('/delete', authenticated, user.delete)
|
||||
usersRouter.post('/me', authenticated, user.fetchUser)
|
||||
usersRouter.post('/follow-user', authenticated, user.follow)
|
||||
usersRouter.post('/like-comment', authenticated, user.likeComment)
|
||||
usersRouter.post('/like-post', authenticated, user.likePost)
|
||||
usersRouter.post('/signup', user.signup)
|
||||
|
||||
// PUT
|
||||
usersRouter.put(
|
||||
'/profile-picture/upload',
|
||||
authenticated,
|
||||
uploadFile,
|
||||
user.uploadPicture,
|
||||
)
|
||||
usersRouter.put('/update-email', authenticated, user.updateEmail)
|
||||
usersRouter.put('/update-name', authenticated, user.updateName)
|
||||
usersRouter.put('/update-password', authenticated, user.updatePassword)
|
||||
|
||||
export default usersRouter
|
|
@ -1,13 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userAuthController(req: Request, res: Response): Promise<void> {
|
||||
const { email, password } = req.body
|
||||
|
||||
const result = await user.auth({ email, password })
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userAuthController
|
|
@ -1,15 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userDeleteController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userId = res.locals.user.id
|
||||
const result = await user.delete(userId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userDeleteController
|
|
@ -1,22 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userFetchInfoController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const username = req.query.u as string
|
||||
|
||||
if (username === undefined) {
|
||||
badRequest(res, 'Missing username')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await user.fetchInfo(username.toLowerCase())
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userFetchInfoController
|
|
@ -1,22 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userFetchPostsController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const username = req.query.u as string
|
||||
|
||||
if (username === undefined) {
|
||||
badRequest(res, 'Missing username')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await user.fetchPosts(username)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userFetchPostsController
|
|
@ -1,22 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userFetchUserController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const id = res.locals.user.id
|
||||
|
||||
if (id === undefined) {
|
||||
badRequest(res, 'Missing id')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await user.fetchUser(id)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userFetchUserController
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userFollowController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userId = res.locals.user.id
|
||||
const { userToFollow } = req.body
|
||||
|
||||
const result = await user.follow(userId, userToFollow)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userFollowController
|
|
@ -1,33 +0,0 @@
|
|||
import userAuthController from './auth'
|
||||
import userDeleteController from './delete'
|
||||
import userFollowController from './follow-user'
|
||||
import userFetchInfoController from './fetch-info'
|
||||
import userFetchPostsController from './fetch-posts'
|
||||
import userFetchUserController from './fetch-user'
|
||||
import userLikeCommentController from './like-comment'
|
||||
import userLikePostController from './like-post'
|
||||
import userSearchController from './search-user'
|
||||
import userSignupController from './signup'
|
||||
import userUpdateEmailController from './update-email'
|
||||
import userUpdateNameController from './update-name'
|
||||
import userUpdatePasswordController from './update-password'
|
||||
import userUploadPictureController from './upload-picture'
|
||||
|
||||
const user = {
|
||||
auth: userAuthController,
|
||||
delete: userDeleteController,
|
||||
fetchInfo: userFetchInfoController,
|
||||
fetchPosts: userFetchPostsController,
|
||||
fetchUser: userFetchUserController,
|
||||
follow: userFollowController,
|
||||
likeComment: userLikeCommentController,
|
||||
likePost: userLikePostController,
|
||||
searchUser: userSearchController,
|
||||
signup: userSignupController,
|
||||
updateEmail: userUpdateEmailController,
|
||||
updateName: userUpdateNameController,
|
||||
updatePassword: userUpdatePasswordController,
|
||||
uploadPicture: userUploadPictureController,
|
||||
} as const
|
||||
|
||||
export default user
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userLikeCommentController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userId = res.locals.user.id
|
||||
const { commentId } = req.body
|
||||
|
||||
const result = await user.likeComment(commentId, userId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userLikeCommentController
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userLikePostController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const userId = res.locals.user.id
|
||||
const { postId } = req.body
|
||||
|
||||
const result = await user.likePost(postId, userId)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userLikePostController
|
|
@ -1,21 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
|
||||
async function userSearchController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const username = req.query.u as string
|
||||
|
||||
if (username === undefined) {
|
||||
badRequest(res, 'Missing username')
|
||||
return
|
||||
}
|
||||
|
||||
const result = await user.searchUser(username)
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
|
||||
export default userSearchController
|
|
@ -1,16 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userSignupController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { username, email, password } = req.body
|
||||
|
||||
const result = await user.signup({ username, email, password })
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userSignupController
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userUpdateEmailController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { email } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
const result = await user.updateEmail({ id, email })
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userUpdateEmailController
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userUpdateNameController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { displayName, username } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
const result = await user.updateName({ id, displayName, username })
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userUpdateNameController
|
|
@ -1,17 +0,0 @@
|
|||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userUpdatePasswordController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
const { currentPassword, newPassword } = req.body
|
||||
const id = res.locals.user.id
|
||||
|
||||
const result = await user.updatePassword(id, currentPassword, newPassword)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userUpdatePasswordController
|
|
@ -1,33 +0,0 @@
|
|||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import user from 'services/users'
|
||||
import type { Request, Response } from 'express'
|
||||
import { badRequest } from 'helpers/http-errors'
|
||||
import handleResponse from 'helpers/handle-response'
|
||||
|
||||
async function userUploadPictureController(
|
||||
req: Request,
|
||||
res: Response,
|
||||
): Promise<void> {
|
||||
if (req.file === undefined) {
|
||||
badRequest(res, 'Expected a JPG or PNG file')
|
||||
return
|
||||
}
|
||||
|
||||
const userId = res.locals.user.id
|
||||
|
||||
let url: string
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
url = `http://${
|
||||
process.env.AWS_BUCKET ?? ''
|
||||
}.s3.localhost.localstack.cloud:4566/${req.file.key}`
|
||||
} else {
|
||||
url = req.file.location
|
||||
}
|
||||
|
||||
const result = await user.uploadPicture(userId, url)
|
||||
|
||||
handleResponse(res, result)
|
||||
}
|
||||
|
||||
export default userUploadPictureController
|
27
src/decorators/create-kweek.decorator.ts
Normal file
27
src/decorators/create-kweek.decorator.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Thanks sandeepsuvit @ https://github.com/nestjs/swagger/issues/417
|
||||
|
||||
import { ApiBody } from "@nestjs/swagger";
|
||||
|
||||
export const ApiCreateKweek =
|
||||
(fieldName: string): MethodDecorator =>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: idk typing for target
|
||||
(target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
|
||||
ApiBody({
|
||||
required: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
content: {
|
||||
type: "string",
|
||||
},
|
||||
[fieldName]: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
format: "binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})(target, propertyKey, descriptor);
|
||||
};
|
4
src/decorators/public.decorator.ts
Normal file
4
src/decorators/public.decorator.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { SetMetadata } from "@nestjs/common";
|
||||
|
||||
export const IS_PUBLIC_KEY = "isPublic";
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
|
@ -1,43 +0,0 @@
|
|||
import sharp from 'sharp'
|
||||
import s3 from 'clients/s3-client'
|
||||
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
|
||||
|
||||
export default async function compressImage(
|
||||
imageName: string,
|
||||
isProfilePicture: string,
|
||||
): Promise<Error | Record<never, never>> {
|
||||
// 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(
|
||||
isProfilePicture === 'true' ? 200 : undefined,
|
||||
isProfilePicture === 'true' ? 200 : undefined,
|
||||
)
|
||||
.jpeg({ quality: 65 })
|
||||
.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')
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
import { type Response } from 'express'
|
||||
import { badRequest } from './http-errors'
|
||||
|
||||
export default function handleResponse(res: Response, result: any): void {
|
||||
if (result instanceof Error) {
|
||||
badRequest(res, result.message)
|
||||
} else {
|
||||
res.json(result)
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { type 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)
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import winston from 'winston'
|
||||
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
debug: 4,
|
||||
}
|
||||
|
||||
const level = (): string => {
|
||||
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions, @typescript-eslint/prefer-nullish-coalescing
|
||||
const env = process.env.NODE_ENV || 'development'
|
||||
const isDevelopment = env === 'development'
|
||||
return isDevelopment ? 'debug' : 'warn'
|
||||
}
|
||||
|
||||
const colors = {
|
||||
error: 'red',
|
||||
warn: 'yellow',
|
||||
info: 'green',
|
||||
http: 'magenta',
|
||||
debug: 'white',
|
||||
}
|
||||
|
||||
winston.addColors(colors)
|
||||
|
||||
const format = winston.format.combine(
|
||||
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
|
||||
winston.format.colorize({ all: true }),
|
||||
winston.format.printf(
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
info => `${info.timestamp} ${info.level}: ${info.message}`,
|
||||
),
|
||||
)
|
||||
|
||||
const transports = [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({
|
||||
filename: 'logs/error.log',
|
||||
level: 'error',
|
||||
}),
|
||||
]
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: level(),
|
||||
levels,
|
||||
format,
|
||||
transports,
|
||||
})
|
||||
|
||||
export default logger
|
|
@ -1,49 +0,0 @@
|
|||
import { type NotificationType } from '@prisma/client'
|
||||
import prisma from 'clients/prisma-client'
|
||||
|
||||
export async function createNotification(
|
||||
fromUserId: string,
|
||||
toUserId: string,
|
||||
content: string,
|
||||
type: NotificationType,
|
||||
): Promise<Record<never, never> | Error> {
|
||||
try {
|
||||
await prisma.notifications.create({
|
||||
data: {
|
||||
type,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
content,
|
||||
},
|
||||
include: {
|
||||
fromUser: {
|
||||
select: {
|
||||
id: true,
|
||||
displayName: true,
|
||||
username: true,
|
||||
profileImage: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return {}
|
||||
} catch (_) {
|
||||
return new Error('Error while creating notification')
|
||||
}
|
||||
}
|
||||
|
||||
export async function countNotifications(
|
||||
toUserId: string,
|
||||
): Promise<number | Error> {
|
||||
try {
|
||||
const count = await prisma.notifications.count({
|
||||
where: {
|
||||
toUserId,
|
||||
},
|
||||
})
|
||||
|
||||
return count
|
||||
} catch (_) {
|
||||
return new Error('Error while counting user notifications')
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import { parse } from 'yaml'
|
||||
import { readFileSync } from 'fs'
|
||||
|
||||
const swaggerConfigFile = readFileSync('./swagger.yaml', 'utf-8')
|
||||
const swaggerDocument = parse(swaggerConfigFile)
|
||||
|
||||
export default swaggerDocument
|
|
@ -1,7 +0,0 @@
|
|||
interface jwtPayload {
|
||||
id: string
|
||||
iat: number
|
||||
exp: number
|
||||
}
|
||||
|
||||
export default jwtPayload
|
|
@ -1,13 +0,0 @@
|
|||
interface User {
|
||||
id?: string
|
||||
displayName?: string | null
|
||||
username?: string
|
||||
email?: string
|
||||
password?: string
|
||||
profileImage?: string | null
|
||||
createdAt?: Date
|
||||
token?: string
|
||||
socketId?: string
|
||||
}
|
||||
|
||||
export default User
|
11
src/kweeks/comments.service.ts
Normal file
11
src/kweeks/comments.service.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||
import { S3Service } from "src/services/s3/s3.service";
|
||||
|
||||
@Injectable()
|
||||
export class CommentsService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly s3: S3Service,
|
||||
) {}
|
||||
}
|
11
src/kweeks/dto/update-kweek.dto.ts
Normal file
11
src/kweeks/dto/update-kweek.dto.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { createZodDto } from "nestjs-zod";
|
||||
import { z } from "nestjs-zod/z";
|
||||
|
||||
export const UpdateKweekSchema = z
|
||||
.object({
|
||||
id: z.string().toLowerCase().describe("New username - optional"),
|
||||
content: z.string({ required_error: "Content is required" }).max(300),
|
||||
})
|
||||
.required();
|
||||
|
||||
export class UpdateKweekDTO extends createZodDto(UpdateKweekSchema) {}
|
1
src/kweeks/entities/kweek.entity.ts
Normal file
1
src/kweeks/entities/kweek.entity.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export class Kweek {}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue