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
|
.env
|
||||||
node_modules/
|
node_modules/
|
||||||
.vscode/
|
.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>
|
DATABASE_URL=postgresql://<placeholder>:<placeholder>@${DB_HOST}:5432/${POSTGRES_DB}?schema=<placeholder>
|
||||||
|
|
||||||
# Redis
|
# 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_PORT=6379
|
||||||
REDIS_PASSWORD=<placehoder>
|
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||||
|
|
||||||
# Express
|
# Fastify
|
||||||
SERVER_PORT=<placeholder>
|
SERVER_PORT=<placeholder>
|
||||||
CLIENT_URL=<placeholder>
|
SERVER_HOST=<placeholder>
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_ACCESS_SECRET=<placeholder>
|
JWT_ACCESS_SECRET=<placeholder>
|
||||||
|
|
||||||
# Localstack - The data can be fake (Change this to real data on production)
|
# Minio
|
||||||
SERVICES=s3
|
MINIO_ROOT_USER=<username>
|
||||||
AWS_ACCESS_KEY_ID=<placeholder>
|
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
MINIO_ENDPOINT=<url>
|
||||||
AWS_DEFAULT_OUTPUT=json
|
|
||||||
|
|
|
@ -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
|
.git
|
||||||
node_modules
|
|
||||||
.env
|
.env
|
||||||
prisma/*.db
|
prisma/*.db
|
||||||
.DS_Store
|
|
||||||
prisma/migrations/dev
|
prisma/migrations/dev
|
||||||
dist
|
|
||||||
pnpm-lock.yaml
|
|
||||||
package_backup.json
|
|
||||||
logs/
|
|
||||||
docker.env
|
docker.env
|
||||||
|
package_copy.json
|
||||||
|
package-lock_copy.json
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
npx --no -- commitlint --edit
|
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
|
|
||||||
}
|
|
36
.swcrc
36
.swcrc
|
@ -1,26 +1,14 @@
|
||||||
{
|
{
|
||||||
"jsc": {
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
"parser": {
|
"sourceMaps": true,
|
||||||
"syntax": "typescript",
|
"jsc": {
|
||||||
"tsx": false,
|
"target": "esnext",
|
||||||
"decorators": true,
|
"parser": {
|
||||||
"dynamicImport": true
|
"syntax": "typescript",
|
||||||
},
|
"decorators": true,
|
||||||
"target": "es2020",
|
"dynamicImport": true
|
||||||
"baseUrl": "./src",
|
},
|
||||||
"paths": {
|
"baseUrl": "./"
|
||||||
"clients/*": ["clients/*"],
|
},
|
||||||
"config/*": ["config/*"],
|
"minify": false
|
||||||
"controllers/*": ["controllers/*"],
|
|
||||||
"interfaces/*": ["interfaces/*"],
|
|
||||||
"helpers/*": ["helpers/*"],
|
|
||||||
"middlewares/*": ["middlewares/*"],
|
|
||||||
"services/*": ["services/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["@types/", "interfaces/"],
|
|
||||||
"module": {
|
|
||||||
"type": "commonjs"
|
|
||||||
},
|
|
||||||
"minify": true
|
|
||||||
}
|
}
|
||||||
|
|
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 /usr/src/app
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY --chown=node:node package*.json ./
|
||||||
COPY prisma ./prisma/
|
COPY --chown=node:node 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
|
|
||||||
|
|
||||||
RUN npm ci
|
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
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023 CookieDasora
|
Copyright (c) 2024 CookieDasora
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
83
README.md
83
README.md
|
@ -1,30 +1,73 @@
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
## Stack
|
A simple RESTful API made with **NestJS** and **Fastify**.
|
||||||
|
|
||||||
**Client**: NextJS, TailwindCSS and Radix UI Icons.
|
### 🚀 Preparing the environment
|
||||||
You can find the front-end [here](https://github.com/CookieDasora/project-knedita-client)
|
|
||||||
|
|
||||||
**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 ✅
|
```bash
|
||||||
- Add post attachments
|
$ npm i
|
||||||
- Create/update/delete Users ✅
|
```
|
||||||
- Password recuperation
|
|
||||||
- Two step verification
|
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.
|
||||||
- Able to choose a profile picture✅
|
|
||||||
- Probably gonna use LocalStack to mock Amazon S3✅
|
You can find the templates for those files on `.env.example` and `docker.env.example`.
|
||||||
- Image compression ✅
|
|
||||||
- Following/unfollowing features ✅
|
To run the necessary services you can execute the following command:
|
||||||
- Like posts ✅
|
|
||||||
- Authentication ✅
|
```bash
|
||||||
- Add more verification (like, if the password is too short) ✅
|
$ npm run docker:db
|
||||||
- Set display name ✅
|
```
|
||||||
- Add rate limit ✅
|
|
||||||
|
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
|
## 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 = {
|
module.exports = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ["@commitlint/config-conventional"],
|
||||||
}
|
};
|
||||||
|
|
|
@ -4,6 +4,9 @@ networks:
|
||||||
localstack-net:
|
localstack-net:
|
||||||
name: localstack-net
|
name: localstack-net
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
minio_network:
|
||||||
|
name: minio_network
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
@ -27,24 +30,23 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- redis:/data
|
||||||
|
|
||||||
localstack:
|
minio:
|
||||||
image: localstack/localstack
|
image: bitnami/minio
|
||||||
container_name: localstack_main
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- localstack-net
|
|
||||||
ports:
|
ports:
|
||||||
- 4566:4566
|
- '9000:9000'
|
||||||
- 4572:4572
|
- '9001:9001'
|
||||||
|
networks:
|
||||||
|
- minio_network
|
||||||
|
volumes:
|
||||||
|
- 'minio_data:/data'
|
||||||
env_file:
|
env_file:
|
||||||
- docker.env
|
- docker.env
|
||||||
volumes:
|
|
||||||
- localstack:/data
|
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
postgres:
|
postgres:
|
||||||
name: backend-db
|
name: backend-db
|
||||||
redis:
|
redis:
|
||||||
driver: local
|
driver: local
|
||||||
localstack:
|
minio_data:
|
||||||
|
driver: local
|
||||||
|
|
|
@ -4,23 +4,11 @@ networks:
|
||||||
localstack-net:
|
localstack-net:
|
||||||
name: localstack-net
|
name: localstack-net
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
minio_network:
|
||||||
|
name: minio_network
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
services:
|
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:
|
postgres:
|
||||||
image: postgres:alpine
|
image: postgres:alpine
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
@ -32,20 +20,18 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- postgres:/var/lib/postgresql/data
|
- postgres:/var/lib/postgresql/data
|
||||||
|
|
||||||
localstack:
|
minio:
|
||||||
image: localstack/localstack
|
image: bitnami/minio
|
||||||
container_name: localstack_main
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- localstack-net
|
|
||||||
ports:
|
ports:
|
||||||
- 4566:4566
|
- '9000:9000'
|
||||||
- 4572:4572
|
- '9001:9001'
|
||||||
|
networks:
|
||||||
|
- minio_network
|
||||||
|
volumes:
|
||||||
|
- 'minio_data:/data'
|
||||||
env_file:
|
env_file:
|
||||||
- docker.env
|
- docker.env
|
||||||
volumes:
|
|
||||||
- localstack:/data
|
|
||||||
- '/var/run/docker.sock:/var/run/docker.sock'
|
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:alpine
|
image: redis:alpine
|
||||||
|
@ -57,9 +43,25 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- redis:/data
|
- 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:
|
volumes:
|
||||||
postgres:
|
postgres:
|
||||||
name: backend-db
|
name: backend-db
|
||||||
redis:
|
redis:
|
||||||
driver: local
|
driver: local
|
||||||
localstack:
|
minio_data:
|
||||||
|
driver: local
|
||||||
|
|
|
@ -5,26 +5,27 @@ NODE_ENV=production
|
||||||
POSTGRES_DB=<placeholder>
|
POSTGRES_DB=<placeholder>
|
||||||
POSTGRES_USER=<placeholder>
|
POSTGRES_USER=<placeholder>
|
||||||
POSTGRES_PASSWORD=<placeholder>
|
POSTGRES_PASSWORD=<placeholder>
|
||||||
|
# Use this hostname on Docker
|
||||||
DB_HOST=postgres
|
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
|
# Redis
|
||||||
|
# Use this hostname on Docker
|
||||||
REDIS_HOST=redis
|
REDIS_HOST=redis
|
||||||
|
|
||||||
REDIS_PORT=<placeholder>
|
REDIS_PORT=<placeholder>
|
||||||
REDIS_PASSWORD=<placeholder>
|
REDIS_PASSWORD=<same_as_defined_in_docker_compose_file>
|
||||||
|
|
||||||
# Express
|
# Fastify
|
||||||
SERVER_PORT=<placeholder>
|
SERVER_PORT=<placeholder>
|
||||||
|
SERVER_HOST=<placeholder>
|
||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_ACCESS_SECRET=<placeholder>
|
JWT_ACCESS_SECRET=<placeholder>
|
||||||
|
|
||||||
# Localstack - The data can be fake
|
# Minio
|
||||||
SERVICES=s3
|
MINIO_ROOT_USER=<username>
|
||||||
AWS_ACCESS_KEY_ID=<placeholder>
|
MINIO_ROOT_PASSWORD=<password_more_or_equal_to_8_characters>
|
||||||
AWS_SECRET_ACCESS_KEY=<placeholder>
|
MINIO_DEFAULT_BUCKETS=<bucket_name>
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
MINIO_ENDPOINT=<url>
|
||||||
AWS_DEFAULT_OUTPUT=json
|
|
||||||
AWS_BUCKET=<placeholder>
|
|
||||||
|
|
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
|
||||||
|
}
|
||||||
|
}
|
14989
package-lock.json
generated
14989
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",
|
"name": "project-knedita",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "A social media",
|
"description": "A open-source social media",
|
||||||
|
"author": "hknsh",
|
||||||
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "swc src -d dist",
|
"build": "nest build",
|
||||||
"dev:start": "ts-node-dev -r tsconfig-paths/register --transpile-only --respawn src/server.ts",
|
"dev:start": "nest start --watch",
|
||||||
|
"dev:debug": "nest start --debug --watch",
|
||||||
"docker": "docker compose --env-file docker.env up -d",
|
"docker": "docker compose --env-file docker.env up -d",
|
||||||
"docker:build": "docker build -t api . && 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:db": "docker compose -f docker-compose.db.yml up -d",
|
||||||
"docker:seed": "docker exec -it api npm run prisma:seed",
|
"lint": "npx @biomejs/biome check --apply .",
|
||||||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts .",
|
"migrate:deploy": "prisma migrate deploy",
|
||||||
"migrate:dev": "prisma migrate dev",
|
"migrate:dev": "prisma migrate dev",
|
||||||
"migrate:dev:create": "prisma migrate dev --create-only",
|
"migrate:dev:create": "prisma migrate dev --create-only",
|
||||||
"migrate:reset": "prisma migrate reset",
|
"migrate:reset": "prisma migrate reset",
|
||||||
"prepare": "husky install",
|
|
||||||
"prisma:generate": "npx prisma generate",
|
"prisma:generate": "npx prisma generate",
|
||||||
"prisma:seed": "prisma db seed",
|
|
||||||
"prisma:studio": "npx prisma studio",
|
"prisma:studio": "npx prisma studio",
|
||||||
"prod:start": "npx prisma migrate deploy && pm2-runtime start dist/server.js",
|
"prepare": "husky",
|
||||||
"test": "vitest run"
|
"prod": "npm run migrate:deploy && node dist/main",
|
||||||
},
|
"start": "nest start",
|
||||||
"ts-standard": {
|
"test": "jest",
|
||||||
"project": "tsconfig.json",
|
"test:cov": "jest --coverage",
|
||||||
"ignore": [
|
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||||
"prisma/*",
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
"dist"
|
"test:watch": "jest --watch"
|
||||||
]
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.4.1",
|
"@aws-sdk/client-s3": "^3.502.0",
|
||||||
"aws-sdk": "^2.1545.0",
|
"@fastify/helmet": "^11.1.1",
|
||||||
"bcrypt": "^5.1.0",
|
"@fastify/multipart": "^8.1.0",
|
||||||
"compression": "^1.7.4",
|
"@fastify/static": "^6.12.0",
|
||||||
"cors": "^2.8.5",
|
"@nest-lab/fastify-multer": "^1.2.0",
|
||||||
"dotenv": "^16.3.2",
|
"@nestjs/common": "^10.0.0",
|
||||||
"express": "^4.18.2",
|
"@nestjs/config": "^3.1.1",
|
||||||
"express-rate-limit": "^7.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",
|
"ioredis": "^5.3.2",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"nestjs-s3": "^2.0.1",
|
||||||
"morgan": "^1.10.0",
|
"nestjs-throttler-storage-redis": "^0.4.1",
|
||||||
"multer": "^1.4.5-lts.1",
|
"nestjs-zod": "^3.0.0",
|
||||||
"multer-s3": "^3.0.1",
|
"passport": "^0.7.0",
|
||||||
"rate-limit-redis": "^4.0.0",
|
"passport-jwt": "^4.0.1",
|
||||||
"redis": "^4.6.7",
|
"passport-local": "^1.0.0",
|
||||||
"sharp": "^0.32.3",
|
"reflect-metadata": "^0.1.13",
|
||||||
"socket.io": "^4.7.2",
|
"rxjs": "^7.8.1",
|
||||||
"swagger-ui-express": "^5.0.0",
|
"sharp": "^0.33.2"
|
||||||
"validator": "^13.9.0",
|
},
|
||||||
"winston": "^3.11.0",
|
"devDependencies": {
|
||||||
"yaml": "^2.3.4"
|
"@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,37 +13,38 @@ model User {
|
||||||
username String @unique
|
username String @unique
|
||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
posts Post[]
|
kweeks Kweek[]
|
||||||
profileImage String?
|
profileImage String?
|
||||||
likedPosts PostLike[]
|
likedKweeks KweekLike[]
|
||||||
likedComments CommentLike[]
|
likedComments CommentLike[]
|
||||||
followers Follows[] @relation("following")
|
followers Follows[] @relation("follower")
|
||||||
following Follows[] @relation("follower")
|
following Follows[] @relation("following")
|
||||||
postComments Comments[]
|
kweeksComments Comments[]
|
||||||
fromNotifications Notifications[] @relation("fromNotifications")
|
fromNotifications Notifications[] @relation("fromNotifications")
|
||||||
toNotifications Notifications[] @relation("toNotifications")
|
toNotifications Notifications[] @relation("toNotifications")
|
||||||
socketId String?
|
socketId String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
model Post {
|
model Kweek {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
content String
|
content String
|
||||||
authorId String
|
authorId String
|
||||||
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||||
likes PostLike[]
|
likes KweekLike[]
|
||||||
comments Comments[]
|
comments Comments[]
|
||||||
createdAt DateTime @default(now())
|
attachments String[]
|
||||||
updatedAt DateTime @updatedAt
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model PostLike {
|
model KweekLike {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
postId String
|
kweekId String
|
||||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// I should join these two up? Yeah, but I will not do it since it didn't work on the first time.
|
// I should join these two up? Yeah, but I will not do it since it didn't work on the first time.
|
||||||
|
@ -71,8 +72,8 @@ model Comments {
|
||||||
content String
|
content String
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
postId String
|
kweekId String
|
||||||
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
|
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||||
likes CommentLike[]
|
likes CommentLike[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt @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