mirror of
https://github.com/hknsh/project-knedita.git
synced 2024-11-28 09:31:16 +00:00
feat: finished routes, renamed a few files and functions
This commit is contained in:
parent
af7745070e
commit
8a55a1938c
38 changed files with 678 additions and 15350 deletions
20
README.md
20
README.md
|
@ -10,12 +10,14 @@ A simple RESTful API made with **NestJS** and **Fastify**.
|
||||||
|
|
||||||
### 🚀 Preparing the environment
|
### 🚀 Preparing the environment
|
||||||
|
|
||||||
Make sure that you have Node, NPM, Docker and Docker Compose installed on your computer.
|
Make sure that you have Bun, Docker and Docker Compose installed on your computer.
|
||||||
|
|
||||||
|
This project also works with Node and Npm.
|
||||||
|
|
||||||
First, install the necessary packages with the following commands:
|
First, install the necessary packages with the following commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm i
|
bun i
|
||||||
```
|
```
|
||||||
|
|
||||||
After that, you can update the `.env` and the `docker.env` files. The `.env` file is for development environment and the `docker.env` is for production.
|
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.
|
||||||
|
@ -25,7 +27,7 @@ You can find the templates for those files on `.env.example` and `docker.env.exa
|
||||||
To run the necessary services you can execute the following command:
|
To run the necessary services you can execute the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run docker:db
|
bun docker:db
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the following services:
|
This will start the following services:
|
||||||
|
@ -37,13 +39,13 @@ This will start the following services:
|
||||||
Apply the migrations to the database with the following command:
|
Apply the migrations to the database with the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run migrate:dev
|
bun migrate:dev
|
||||||
```
|
```
|
||||||
|
|
||||||
And now, you can start the server with the command:
|
And now, you can start the server with the command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run dev:start
|
bun dev:start
|
||||||
```
|
```
|
||||||
|
|
||||||
You can check the documentation accessing the endpoint `/` in your browser
|
You can check the documentation accessing the endpoint `/` in your browser
|
||||||
|
@ -51,7 +53,7 @@ You can check the documentation accessing the endpoint `/` in your browser
|
||||||
To run in production you can use the following command:
|
To run in production you can use the following command:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ npm run docker
|
bun docker
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start all the previous services and the back-end image.
|
This will start all the previous services and the back-end image.
|
||||||
|
@ -60,11 +62,11 @@ This will start all the previous services and the back-end image.
|
||||||
|
|
||||||
This back-end uses the following stack:
|
This back-end uses the following stack:
|
||||||
|
|
||||||
- **Fastify**
|
|
||||||
- **MinIO**
|
|
||||||
- **NestJS**
|
- **NestJS**
|
||||||
- **PostgreSQL**
|
- **Fastify**
|
||||||
- **Prisma**
|
- **Prisma**
|
||||||
|
- **MinIO**
|
||||||
|
- **PostgreSQL**
|
||||||
- **Redis**
|
- **Redis**
|
||||||
- **Swagger**
|
- **Swagger**
|
||||||
- **Typescript**
|
- **Typescript**
|
||||||
|
|
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
|
@ -1,5 +1,3 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
localstack-net:
|
localstack-net:
|
||||||
name: localstack-net
|
name: localstack-net
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
version: '3.8'
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
localstack-net:
|
localstack-net:
|
||||||
name: localstack-net
|
name: localstack-net
|
||||||
|
|
15114
package-lock.json
generated
15114
package-lock.json
generated
File diff suppressed because it is too large
Load diff
73
package.json
73
package.json
|
@ -28,63 +28,62 @@
|
||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.502.0",
|
"@aws-sdk/client-s3": "^3.664.0",
|
||||||
"@fastify/helmet": "^11.1.1",
|
"@fastify/helmet": "^11.1.1",
|
||||||
"@fastify/multipart": "^8.1.0",
|
"@fastify/multipart": "^8.3.0",
|
||||||
"@fastify/static": "^6.12.0",
|
"@fastify/static": "^6.12.0",
|
||||||
"@nest-lab/fastify-multer": "^1.2.0",
|
"@nest-lab/fastify-multer": "^1.2.0",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nest-lab/throttler-storage-redis": "^1.0.0",
|
||||||
"@nestjs/config": "^3.1.1",
|
"@nestjs/common": "^10.4.4",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/config": "^3.2.3",
|
||||||
|
"@nestjs/core": "^10.4.4",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
"@nestjs/passport": "^10.0.3",
|
"@nestjs/passport": "^10.0.3",
|
||||||
"@nestjs/platform-fastify": "^10.3.1",
|
"@nestjs/platform-fastify": "^10.4.4",
|
||||||
"@nestjs/swagger": "^7.2.0",
|
"@nestjs/swagger": "^7.4.2",
|
||||||
"@nestjs/throttler": "^5.1.1",
|
"@nestjs/throttler": "6.2.1",
|
||||||
"@prisma/client": "^5.9.1",
|
"@prisma/client": "^5.20.0",
|
||||||
"bcrypt": "^5.1.1",
|
"argon2": "^0.41.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"dotenv-expand": "^11.0.6",
|
"dotenv-expand": "^11.0.6",
|
||||||
"file-type": "^19.0.0",
|
"file-type": "^19.5.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.4.1",
|
||||||
"nestjs-s3": "^2.0.1",
|
"nestjs-s3": "^2.0.1",
|
||||||
"nestjs-throttler-storage-redis": "^0.4.1",
|
|
||||||
"nestjs-zod": "^3.0.0",
|
"nestjs-zod": "^3.0.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.5",
|
||||||
"tstl": "^3.0.0"
|
"tstl": "^3.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.5.3",
|
"@biomejs/biome": "1.5.3",
|
||||||
"@commitlint/cli": "^18.6.0",
|
"@commitlint/cli": "^18.6.1",
|
||||||
"@commitlint/config-conventional": "^18.6.0",
|
"@commitlint/config-conventional": "^18.6.3",
|
||||||
"@nestjs/cli": "^10.3.2",
|
"@nestjs/cli": "^10.4.5",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.1.4",
|
||||||
"@nestjs/testing": "^10.3.3",
|
"@nestjs/testing": "^10.4.4",
|
||||||
"@swc/cli": "^0.1.65",
|
"@swc/cli": "^0.1.65",
|
||||||
"@swc/core": "^1.3.107",
|
"@swc/core": "^1.7.26",
|
||||||
"@swc/jest": "^0.2.36",
|
"@swc/jest": "^0.2.36",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/node": "^20.16.10",
|
||||||
"@types/node": "^20.12.7",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-jwt": "^4.0.0",
|
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.2",
|
||||||
"husky": "^9.0.7",
|
"husky": "^9.1.6",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^15.2.1",
|
"lint-staged": "^15.2.10",
|
||||||
"prisma": "^5.12.1",
|
"prisma": "^5.20.0",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.4",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-loader": "^9.4.3",
|
"ts-loader": "^9.5.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.6.2"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
@ -107,5 +106,9 @@
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"**/*.@(js|ts|json)": "biome check --apply --no-errors-on-unmatched"
|
"**/*.@(js|ts|json)": "biome check --apply --no-errors-on-unmatched"
|
||||||
}
|
},
|
||||||
|
"trustedDependencies": [
|
||||||
|
"@biomejs/biome",
|
||||||
|
"@nestjs/core"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Comments" ADD COLUMN "attachments" TEXT[];
|
|
@ -0,0 +1,6 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Comments" ADD COLUMN "parentId" TEXT,
|
||||||
|
ALTER COLUMN "kweekId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Comments" ADD CONSTRAINT "Comments_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Comments"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
@ -72,11 +72,16 @@ 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)
|
||||||
kweekId String
|
kweekId String?
|
||||||
kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
kweek Kweek? @relation(fields: [kweekId], references: [id], onDelete: Cascade)
|
||||||
likes CommentLike[]
|
likes CommentLike[]
|
||||||
|
attachments String[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt @default(now())
|
updatedAt DateTime @updatedAt @default(now())
|
||||||
|
|
||||||
|
parentId String?
|
||||||
|
parent Comments? @relation("CommentReplies", fields: [parentId], references: [id])
|
||||||
|
replies Comments[] @relation("CommentReplies")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Notifications {
|
model Notifications {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
import { FastifyMulterModule } from "@nest-lab/fastify-multer";
|
||||||
|
import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { ConfigModule } from "@nestjs/config";
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
import { APP_GUARD, APP_PIPE } from "@nestjs/core";
|
||||||
import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler";
|
import { ThrottlerGuard, ThrottlerModule, seconds } from "@nestjs/throttler";
|
||||||
import { S3Module } from "nestjs-s3";
|
import { S3Module } from "nestjs-s3";
|
||||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
|
||||||
import { ZodValidationPipe } from "nestjs-zod";
|
import { ZodValidationPipe } from "nestjs-zod";
|
||||||
import { AuthModule } from "./auth/auth.module";
|
import { AuthModule } from "./auth/auth.module";
|
||||||
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
import { JwtAuthGuard } from "./auth/jwt-auth.guard";
|
||||||
|
@ -14,17 +14,18 @@ import { UserModule } from "./users/users.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UserModule,
|
ThrottlerModule.forRoot({
|
||||||
AuthModule,
|
throttlers: [{ limit: 10, ttl: seconds(60) }],
|
||||||
|
storage: new ThrottlerStorageRedisService(Configuration.REDIS_URL()),
|
||||||
|
errorMessage: "Too many requests",
|
||||||
|
}),
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
isGlobal: true,
|
isGlobal: true,
|
||||||
}),
|
}),
|
||||||
ThrottlerModule.forRoot({
|
|
||||||
throttlers: [{ limit: 10, ttl: 60000 }],
|
|
||||||
storage: new ThrottlerStorageRedisService(Configuration.REDIS_URL()),
|
|
||||||
}),
|
|
||||||
KweeksModule,
|
|
||||||
FastifyMulterModule,
|
FastifyMulterModule,
|
||||||
|
UserModule,
|
||||||
|
KweeksModule,
|
||||||
|
AuthModule,
|
||||||
S3Module.forRoot({
|
S3Module.forRoot({
|
||||||
config: {
|
config: {
|
||||||
credentials: {
|
credentials: {
|
||||||
|
@ -38,14 +39,14 @@ import { UserModule } from "./users/users.module";
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
|
||||||
provide: APP_PIPE,
|
|
||||||
useClass: ZodValidationPipe,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: ThrottlerGuard,
|
useClass: ThrottlerGuard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: APP_PIPE,
|
||||||
|
useClass: ZodValidationPipe,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: JwtAuthGuard,
|
useClass: JwtAuthGuard,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as argon2 from "argon2";
|
||||||
import { UserModel } from "src/users/models/user.model";
|
import { UserModel } from "src/users/models/user.model";
|
||||||
import { UserService } from "src/users/users.service";
|
import { UserService } from "src/users/users.service";
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ export class AuthService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validation = await bcrypt.compare(password, user.password);
|
const validation = await argon2.verify(user.password, password);
|
||||||
|
|
||||||
if (user && validation) {
|
if (user && validation) {
|
||||||
const { password, ...result } = user;
|
const { password, ...result } = user;
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
// Thanks sandeepsuvit @ https://github.com/nestjs/swagger/issues/417
|
|
||||||
// TODO: Remove this decorator.
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
85
src/kweeks/comments.controller.ts
Normal file
85
src/kweeks/comments.controller.ts
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import { File, FilesInterceptor } from "@nest-lab/fastify-multer";
|
||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Request,
|
||||||
|
UploadedFiles,
|
||||||
|
UseInterceptors,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
|
ApiConsumes,
|
||||||
|
ApiOperation,
|
||||||
|
ApiTags,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { Public } from "src/decorators/public.decorator";
|
||||||
|
import { MultiFileValidation } from "src/validators/multi_file.validator";
|
||||||
|
import { CommentsService } from "./comments.service";
|
||||||
|
import { UpdateCommentDTO } from "./dto/comments/update_comment.dto";
|
||||||
|
import { AttachmentsSchema } from "./schemas/attachments.schema";
|
||||||
|
|
||||||
|
@ApiTags("Kweeks")
|
||||||
|
@Controller("kweeks")
|
||||||
|
export class CommentsController {
|
||||||
|
constructor(private readonly commentsService: CommentsService) {}
|
||||||
|
|
||||||
|
@Post(":id/comments")
|
||||||
|
@ApiConsumes("multipart/form-data")
|
||||||
|
@ApiBody(AttachmentsSchema)
|
||||||
|
@UseInterceptors(FilesInterceptor("attachments", 4))
|
||||||
|
@ApiOperation({ summary: "Creates a comment" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
@ApiBadRequestResponse({ description: "Content too long" })
|
||||||
|
create(
|
||||||
|
@UploadedFiles(new MultiFileValidation()) attachments: Array<File>,
|
||||||
|
@Body() body,
|
||||||
|
@Request() req,
|
||||||
|
@Param("id") id: string,
|
||||||
|
) {
|
||||||
|
return this.commentsService.create(
|
||||||
|
id,
|
||||||
|
body.content,
|
||||||
|
req.user.id,
|
||||||
|
attachments,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get("comments/:comment_id")
|
||||||
|
@ApiOperation({ summary: "Retrieves information about a comment" })
|
||||||
|
comment(@Param("comment_id") comment_id: string) {
|
||||||
|
return this.commentsService.info(comment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id/comments/:comment_id")
|
||||||
|
@ApiOperation({ summary: "Updates a comment content" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
updateComment(
|
||||||
|
@Param("comment_id") comment_id: string,
|
||||||
|
@Request() req,
|
||||||
|
@Body() body: UpdateCommentDTO,
|
||||||
|
) {
|
||||||
|
return this.commentsService.update(comment_id, req.user.id, body.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete("comments/:comment_id")
|
||||||
|
@ApiOperation({ summary: "Deletes a comment" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
removeComment(@Param("comment_id") comment_id: string, @Request() req) {
|
||||||
|
return this.commentsService.delete(comment_id, req.user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("comments/:comment_id/like")
|
||||||
|
@ApiOperation({ summary: "Likes a comment" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
likeComment(@Param("comment_id") comment_id: string, @Request() req) {
|
||||||
|
return this.commentsService.like(comment_id, req.user.id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,13 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { selectCommentsWithReplies } from "./schemas/prisma_queries.schema";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CommentsService {
|
export class CommentsService {
|
||||||
|
@ -8,4 +15,171 @@ export class CommentsService {
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly s3: S3Service,
|
private readonly s3: S3Service,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
kweek_id: string,
|
||||||
|
content: string,
|
||||||
|
user_id: string,
|
||||||
|
files: Array<File>,
|
||||||
|
) {
|
||||||
|
if (content.length > 300) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
"Content too big. Must have 300 characters or lower",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verifies if the kweek_id is a kweek or a comment
|
||||||
|
const parentComment = await this.prisma.comments.findUnique({
|
||||||
|
where: { id: kweek_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
let kweek = null;
|
||||||
|
|
||||||
|
if (parentComment === null) {
|
||||||
|
kweek = await this.prisma.kweek.findFirst({
|
||||||
|
where: { id: kweek_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kweek === null) {
|
||||||
|
throw new NotFoundException("Kweek/Comment not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id } = await this.prisma.comments.create({
|
||||||
|
data: {
|
||||||
|
content,
|
||||||
|
userId: user_id,
|
||||||
|
kweekId: kweek ? kweek.id : null,
|
||||||
|
parentId: parentComment ? parentComment.id : null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let attachments = [];
|
||||||
|
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
attachments = await this.s3.multiImageUpload(id, files);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prisma.comments.update({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
attachments,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return await this.prisma.comments.findFirst({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async info(comment_id: string) {
|
||||||
|
const comment = await this.prisma.comments.findUnique({
|
||||||
|
where: { id: comment_id },
|
||||||
|
select: {
|
||||||
|
...selectCommentsWithReplies,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment === null) {
|
||||||
|
throw new NotFoundException("Comment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(comment_id: string, user_id: string, content: string) {
|
||||||
|
let new_content = content;
|
||||||
|
|
||||||
|
const comment = await this.prisma.comments.findFirst({
|
||||||
|
where: { id: comment_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment === null) {
|
||||||
|
throw new NotFoundException("Comment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.userId !== user_id) {
|
||||||
|
throw new UnauthorizedException("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.content === content.trim()) {
|
||||||
|
new_content = comment.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.prisma.comments.update({
|
||||||
|
where: {
|
||||||
|
id: comment_id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
content: new_content,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...selectCommentsWithReplies,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(comment_id: string, user_id: string) {
|
||||||
|
const comment = await this.prisma.comments.findFirst({
|
||||||
|
where: { id: comment_id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment === null) {
|
||||||
|
throw new NotFoundException("Comment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.userId !== user_id) {
|
||||||
|
throw new UnauthorizedException("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.s3.deleteFiles(comment.attachments);
|
||||||
|
|
||||||
|
await this.prisma.comments.delete({
|
||||||
|
where: {
|
||||||
|
id: comment_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async like(comment_id: string, user_id: string) {
|
||||||
|
const comment = await this.prisma.comments.findFirst({
|
||||||
|
where: {
|
||||||
|
id: comment_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (comment === null) {
|
||||||
|
throw new NotFoundException("Comment not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_comment_already_liked = await this.prisma.commentLike.findFirst({
|
||||||
|
where: {
|
||||||
|
commentId: comment.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is_comment_already_liked !== null) {
|
||||||
|
await this.prisma.commentLike.deleteMany({
|
||||||
|
where: {
|
||||||
|
commentId: comment.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.prisma.commentLike.create({
|
||||||
|
data: {
|
||||||
|
commentId: comment.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
10
src/kweeks/dto/comments/update_comment.dto.ts
Normal file
10
src/kweeks/dto/comments/update_comment.dto.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { createZodDto } from "nestjs-zod";
|
||||||
|
import { z } from "nestjs-zod/z";
|
||||||
|
|
||||||
|
export const UpdateCommentSchema = z
|
||||||
|
.object({
|
||||||
|
content: z.string({ required_error: "Content is required" }).max(300),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export class UpdateCommentDTO extends createZodDto(UpdateCommentSchema) {}
|
|
@ -3,7 +3,7 @@ import { z } from "nestjs-zod/z";
|
||||||
|
|
||||||
export const UpdateKweekSchema = z
|
export const UpdateKweekSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.string().toLowerCase().describe("New username - optional"),
|
post_id: z.string({ required_error: "Post ID is required" }),
|
||||||
content: z.string({ required_error: "Content is required" }).max(300),
|
content: z.string({ required_error: "Content is required" }).max(300),
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
|
@ -1 +0,0 @@
|
||||||
export class Kweek {}
|
|
|
@ -12,15 +12,18 @@ import {
|
||||||
UseInterceptors,
|
UseInterceptors,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
|
ApiBadRequestResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
|
ApiBody,
|
||||||
ApiConsumes,
|
ApiConsumes,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { ApiCreateKweek } from "src/decorators/create-kweek.decorator";
|
|
||||||
import { Public } from "src/decorators/public.decorator";
|
import { Public } from "src/decorators/public.decorator";
|
||||||
import { MultiFileValidation } from "src/validators/multi-file.validator";
|
import { MultiFileValidation } from "src/validators/multi_file.validator";
|
||||||
|
import { UpdateKweekDTO } from "./dto/kweeks/update_kweek.dto";
|
||||||
import { KweeksService } from "./kweeks.service";
|
import { KweeksService } from "./kweeks.service";
|
||||||
|
import { AttachmentsSchema } from "./schemas/attachments.schema";
|
||||||
|
|
||||||
@ApiTags("Kweeks")
|
@ApiTags("Kweeks")
|
||||||
@Controller("kweeks")
|
@Controller("kweeks")
|
||||||
|
@ -29,10 +32,11 @@ export class KweeksController {
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiConsumes("multipart/form-data")
|
@ApiConsumes("multipart/form-data")
|
||||||
@ApiCreateKweek("attachments")
|
@ApiBody(AttachmentsSchema)
|
||||||
@UseInterceptors(FilesInterceptor("attachments", 4))
|
@UseInterceptors(FilesInterceptor("attachments", 4))
|
||||||
@ApiOperation({ summary: "Creates a kweek" })
|
@ApiOperation({ summary: "Creates a kweek" })
|
||||||
@ApiBearerAuth("JWT")
|
@ApiBearerAuth("JWT")
|
||||||
|
@ApiBadRequestResponse({ description: "Content too long" })
|
||||||
create(
|
create(
|
||||||
@UploadedFiles(new MultiFileValidation()) attachments: Array<File>,
|
@UploadedFiles(new MultiFileValidation()) attachments: Array<File>,
|
||||||
@Body() body,
|
@Body() body,
|
||||||
|
@ -51,44 +55,21 @@ export class KweeksController {
|
||||||
@Patch()
|
@Patch()
|
||||||
@ApiOperation({ summary: "Updates a kweek content" })
|
@ApiOperation({ summary: "Updates a kweek content" })
|
||||||
@ApiBearerAuth("JWT")
|
@ApiBearerAuth("JWT")
|
||||||
update(@Body() body: { id: string; content: string }) {
|
update(@Body() body: UpdateKweekDTO, @Request() req) {
|
||||||
return this.kweeksService.update(body.id, body.content);
|
return this.kweeksService.update(req.user.id, body.post_id, body.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
@ApiOperation({ summary: "Deletes a kweek" })
|
@ApiOperation({ summary: "Deletes a kweek" })
|
||||||
@ApiBearerAuth("JWT")
|
@ApiBearerAuth("JWT")
|
||||||
remove(@Param("id") id: string) {
|
remove(@Param("id") id: string, @Request() req) {
|
||||||
return this.kweeksService.remove(id);
|
return this.kweeksService.remove(req.user.id, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":id/like")
|
@Post(":id/like")
|
||||||
@ApiOperation({ summary: "Likes a kweek" })
|
@ApiOperation({ summary: "Likes a kweek" })
|
||||||
@ApiBearerAuth("JWT")
|
@ApiBearerAuth("JWT")
|
||||||
likeKweek() {}
|
likeKweek(@Param("id") kweek_id: string, @Request() req) {
|
||||||
|
return this.kweeksService.like(req.user.id, kweek_id);
|
||||||
@Public()
|
}
|
||||||
@Get(":id/comments")
|
|
||||||
@ApiOperation({ summary: "Retrieves comments of a kweek" })
|
|
||||||
comments() {}
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Get(":id/comments/:comment_id")
|
|
||||||
@ApiOperation({ summary: "Retrieves information about a comment" })
|
|
||||||
comment() {}
|
|
||||||
|
|
||||||
@Patch(":id/comments/:comment_id")
|
|
||||||
@ApiOperation({ summary: "Updates a comment content" })
|
|
||||||
@ApiBearerAuth("JWT")
|
|
||||||
updateComment() {}
|
|
||||||
|
|
||||||
@Delete(":id/comments/:comment_id")
|
|
||||||
@ApiOperation({ summary: "Deletes a comment" })
|
|
||||||
@ApiBearerAuth("JWT")
|
|
||||||
removeComment() {}
|
|
||||||
|
|
||||||
@Post(":id/comments/:comment_id/like")
|
|
||||||
@ApiOperation({ summary: "Likes a comment" })
|
|
||||||
@ApiBearerAuth("JWT")
|
|
||||||
likeComment() {}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from "src/services/prisma/prisma.module";
|
import { PrismaModule } from "src/services/prisma/prisma.module";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { CommentsController } from "./comments.controller";
|
||||||
import { CommentsService } from "./comments.service";
|
import { CommentsService } from "./comments.service";
|
||||||
import { KweeksController } from "./kweeks.controller";
|
import { KweeksController } from "./kweeks.controller";
|
||||||
import { KweeksService } from "./kweeks.service";
|
import { KweeksService } from "./kweeks.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
controllers: [KweeksController],
|
controllers: [KweeksController, CommentsController],
|
||||||
providers: [KweeksService, S3Service, CommentsService],
|
providers: [KweeksService, S3Service, CommentsService],
|
||||||
})
|
})
|
||||||
export class KweeksModule {}
|
export class KweeksModule {}
|
||||||
|
|
|
@ -3,9 +3,11 @@ import {
|
||||||
BadRequestException,
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
|
import { selectComments, selectUser } from "./schemas/prisma_queries.schema";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class KweeksService {
|
export class KweeksService {
|
||||||
|
@ -30,7 +32,11 @@ export class KweeksService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const attachments = await this.s3.multiImageUploadToMinio(id, files);
|
let attachments = [];
|
||||||
|
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
attachments = await this.s3.multiImageUpload(id, files);
|
||||||
|
}
|
||||||
|
|
||||||
await this.prisma.kweek.update({
|
await this.prisma.kweek.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -49,21 +55,13 @@ export class KweeksService {
|
||||||
where: {
|
where: {
|
||||||
id,
|
id,
|
||||||
},
|
},
|
||||||
select: {
|
include: {
|
||||||
id: true,
|
author: selectUser,
|
||||||
content: true,
|
_count: {
|
||||||
attachments: true,
|
select: { comments: true, likes: true },
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
author: {
|
|
||||||
select: {
|
|
||||||
displayName: true,
|
|
||||||
username: true,
|
|
||||||
profileImage: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
likes: true,
|
likes: true,
|
||||||
comments: true,
|
comments: selectComments,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,11 +72,102 @@ export class KweeksService {
|
||||||
return post;
|
return post;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: string, content: string) {
|
async update(user_id: string, post_id: string, content: string) {
|
||||||
return `This action updates a #${id} kweek`;
|
let new_content = content;
|
||||||
|
|
||||||
|
const post = await this.prisma.kweek.findFirst({ where: { id: post_id } });
|
||||||
|
|
||||||
|
if (post === null) {
|
||||||
|
throw new NotFoundException("Post not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(id: string) {
|
if (post.authorId !== user_id) {
|
||||||
return `This action removes a #${id} kweek`;
|
throw new UnauthorizedException("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.content === content.trim()) {
|
||||||
|
new_content = post.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.prisma.kweek.update({
|
||||||
|
where: {
|
||||||
|
id: post_id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
content: new_content,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
attachments: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
author: {
|
||||||
|
select: {
|
||||||
|
displayName: true,
|
||||||
|
username: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(user_id: string, id: string) {
|
||||||
|
const post = await this.prisma.kweek.findFirst({ where: { id } });
|
||||||
|
|
||||||
|
if (post === null) {
|
||||||
|
throw new NotFoundException("Post not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (post.authorId !== user_id) {
|
||||||
|
throw new UnauthorizedException("Forbidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.s3.deleteFiles(post.attachments);
|
||||||
|
|
||||||
|
await this.prisma.kweek.delete({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async like(user_id: string, kweek_id: string) {
|
||||||
|
const kweek = await this.prisma.kweek.findFirst({
|
||||||
|
where: {
|
||||||
|
id: kweek_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (kweek === null) {
|
||||||
|
throw new NotFoundException("Post not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_kweek_already_liked = await this.prisma.kweekLike.findFirst({
|
||||||
|
where: {
|
||||||
|
kweekId: kweek.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is_kweek_already_liked !== null) {
|
||||||
|
await this.prisma.kweekLike.deleteMany({
|
||||||
|
where: {
|
||||||
|
kweekId: kweek.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.prisma.kweekLike.create({
|
||||||
|
data: {
|
||||||
|
kweekId: kweek.id,
|
||||||
|
userId: user_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
src/kweeks/schemas/attachments.schema.ts
Normal file
18
src/kweeks/schemas/attachments.schema.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
export const AttachmentsSchema = {
|
||||||
|
required: true,
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
content: {
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
attachments: {
|
||||||
|
type: "array",
|
||||||
|
items: {
|
||||||
|
type: "string",
|
||||||
|
format: "binary",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
50
src/kweeks/schemas/prisma_queries.schema.ts
Normal file
50
src/kweeks/schemas/prisma_queries.schema.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
export const selectUser = {
|
||||||
|
select: {
|
||||||
|
displayName: true,
|
||||||
|
username: true,
|
||||||
|
profileImage: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectReplies = {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
parentId: true,
|
||||||
|
attachments: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
user: selectUser,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectCommentsWithReplies = {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
kweekId: true,
|
||||||
|
attachments: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
user: selectUser,
|
||||||
|
_count: {
|
||||||
|
select: { replies: true, likes: true },
|
||||||
|
},
|
||||||
|
replies: {
|
||||||
|
...selectReplies,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectComments = {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
content: true,
|
||||||
|
kweekId: true,
|
||||||
|
attachments: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
user: selectUser,
|
||||||
|
_count: {
|
||||||
|
select: { replies: true, likes: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
25
src/main.ts
25
src/main.ts
|
@ -12,35 +12,14 @@ import { Configuration } from "./configuration";
|
||||||
/*
|
/*
|
||||||
--- Present ---
|
--- Present ---
|
||||||
|
|
||||||
TODO: Remove the eval on buffer type validator. <--- TOP PRIORITY. THIS IS DANGEROUS. <-- Downgrade to 16.5.4 is the solution.
|
|
||||||
TODO: Find a way to get the static url of the uploaded file.
|
|
||||||
TODO: Remove single file upload function.
|
|
||||||
TODO: Remove `create-kweek` decorator.
|
|
||||||
TODO: Finish some routes.
|
TODO: Finish some routes.
|
||||||
-> Kweek/Comments routes needs to be finished.
|
|
||||||
-> Delete User service needs more protection.
|
-> Delete User service needs more protection.
|
||||||
TODO: Create tests. <- Check NestJS documentation for that.
|
|
||||||
TODO: Add `user` type to @nestjs/common ---> Request.
|
TODO: Add `user` type to @nestjs/common ---> Request.
|
||||||
TODO: Remove some useless information on `README`.
|
|
||||||
TODO: Check if the Dockerfile is still working.
|
|
||||||
TODO: Replace Prisma to Kysely or something more low-level.
|
|
||||||
TODO: Replace Zod to Typebox.
|
|
||||||
|
|
||||||
--- Future ---
|
|
||||||
|
|
||||||
TODO: Kubernetes.
|
|
||||||
TODO: Send e-mails to the user when something happens to his account.
|
|
||||||
TODO: Add a authorization system.
|
TODO: Add a authorization system.
|
||||||
TODO: Create a administrator dashboard showing statistics of the platform. <- Needs front-end first I guess...
|
TODO: Send e-mails to the user when something happens to his account.
|
||||||
-> Only users with moderation/administration permission will be able to access it.
|
|
||||||
-> Users with moderation role can't access the statistics.
|
|
||||||
-> They will need the permission of an administrator to delete kweeks and users.
|
|
||||||
-> These users will be able to delete kweeks and terminate accounts with a obligatory reason.
|
|
||||||
-> This reason will be send to the person by e-mail and he can contest this decision.
|
|
||||||
TODO: Create a TOS.
|
|
||||||
TODO: Create the chat system.
|
TODO: Create the chat system.
|
||||||
-> Initialize the websocket system first.
|
-> Initialize the websocket system first.
|
||||||
TODO: Check compatibility with Bun.
|
TODO: Create a TOS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { PutObjectCommand, PutObjectCommandInput } from "@aws-sdk/client-s3";
|
import {
|
||||||
|
DeleteObjectsCommand,
|
||||||
|
PutObjectCommand,
|
||||||
|
PutObjectCommandInput,
|
||||||
|
} from "@aws-sdk/client-s3";
|
||||||
import { File } from "@nest-lab/fastify-multer";
|
import { File } from "@nest-lab/fastify-multer";
|
||||||
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
import { Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||||
import { InjectS3, S3 } from "nestjs-s3";
|
import { InjectS3, S3 } from "nestjs-s3";
|
||||||
|
@ -7,6 +11,9 @@ import { Configuration } from "src/configuration";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class S3Service {
|
export class S3Service {
|
||||||
|
private bucket: string = Configuration.MINIO_DEFAULT_BUCKETS();
|
||||||
|
private endpoint: string = Configuration.MINIO_ENDPOINT();
|
||||||
|
|
||||||
constructor(@InjectS3() private readonly s3: S3) {}
|
constructor(@InjectS3() private readonly s3: S3) {}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -16,16 +23,16 @@ export class S3Service {
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the image url if the upload to minio was successful.
|
* Returns the image url if the upload was successful.
|
||||||
*/
|
*/
|
||||||
async uploadImageToMinio(userID: string, buffer: Buffer): Promise<string> {
|
async uploadImage(userID: string, buffer: Buffer): Promise<string> {
|
||||||
const compressedBuffer = await sharp(buffer)
|
const compressedBuffer = await sharp(buffer)
|
||||||
.resize(200, 200)
|
.resize(200, 200)
|
||||||
.webp({ quality: 70 })
|
.webp({ quality: 70 })
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
const params: PutObjectCommandInput = {
|
const params: PutObjectCommandInput = {
|
||||||
Bucket: Configuration.MINIO_DEFAULT_BUCKETS(),
|
Bucket: this.bucket,
|
||||||
Key: `profile_images/${userID}.webp`,
|
Key: `profile_images/${userID}.webp`,
|
||||||
Body: compressedBuffer,
|
Body: compressedBuffer,
|
||||||
ContentType: "image/webp",
|
ContentType: "image/webp",
|
||||||
|
@ -36,7 +43,7 @@ export class S3Service {
|
||||||
const { ETag } = await this.s3.send(new PutObjectCommand(params));
|
const { ETag } = await this.s3.send(new PutObjectCommand(params));
|
||||||
|
|
||||||
if (ETag !== null) {
|
if (ETag !== null) {
|
||||||
return `${Configuration.MINIO_ENDPOINT}/${Configuration.MINIO_DEFAULT_BUCKETS}/profile_images/${userID}.webp`;
|
return `${this.endpoint}/${this.bucket}/profile_images/${userID}.webp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InternalServerErrorException(
|
throw new InternalServerErrorException(
|
||||||
|
@ -44,7 +51,7 @@ export class S3Service {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async multiImageUploadToMinio(id: string, files: Array<File>) {
|
async multiImageUpload(id: string, files: Array<File>) {
|
||||||
const buffers: Buffer[] = [];
|
const buffers: Buffer[] = [];
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
|
@ -61,23 +68,60 @@ export class S3Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadPromises = buffers.map(async (buffer, index) => {
|
const uploadPromises = buffers.map(async (buffer, index) => {
|
||||||
return await this.multiUploadToMinio(buffer, id, index + 1);
|
return await this.multiUpload(buffer, id, index + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(uploadPromises);
|
return Promise.all(uploadPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async multiUploadToMinio(buffer: Buffer, id: string, index: number) {
|
private async multiUpload(buffer: Buffer, id: string, index: number) {
|
||||||
const Key = `posts/${id}/${index}.webp`;
|
const Key = `posts/${id}/${index}.webp`;
|
||||||
|
|
||||||
const params: PutObjectCommandInput = {
|
const params: PutObjectCommandInput = {
|
||||||
Bucket: Configuration.MINIO_DEFAULT_BUCKETS(),
|
Bucket: this.bucket,
|
||||||
Key,
|
Key,
|
||||||
Body: buffer,
|
Body: buffer,
|
||||||
ContentType: "image/webp",
|
ContentType: "image/webp",
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.s3.send(new PutObjectCommand(params));
|
await this.s3.send(new PutObjectCommand(params));
|
||||||
return `${Configuration.MINIO_ENDPOINT}/${Configuration.MINIO_DEFAULT_BUCKETS}/${Key}`;
|
return `${this.endpoint}/${this.bucket}/${Key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFiles(attachments: string[]) {
|
||||||
|
if (attachments.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys = attachments.map((url) => {
|
||||||
|
try {
|
||||||
|
const parsed_url = new URL(url);
|
||||||
|
let key = parsed_url.pathname.substring(1);
|
||||||
|
|
||||||
|
if (key.startsWith(`${this.bucket}/`)) {
|
||||||
|
key = key.replace(`${this.bucket}`, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
} catch (e) {
|
||||||
|
throw new InternalServerErrorException("Failed to parse URL");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new DeleteObjectsCommand({
|
||||||
|
Bucket: this.bucket,
|
||||||
|
Delete: {
|
||||||
|
Objects: keys.map((key) => ({ Key: key })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.s3.send(command);
|
||||||
|
} catch (error) {
|
||||||
|
throw new InternalServerErrorException(
|
||||||
|
"Failed to delete files from S3:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
16
src/users/dto/follow_user.dto.ts
Normal file
16
src/users/dto/follow_user.dto.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { createZodDto } from "nestjs-zod";
|
||||||
|
import { z } from "nestjs-zod/z";
|
||||||
|
|
||||||
|
export const FollowUserSchema = z
|
||||||
|
.object({
|
||||||
|
username: z
|
||||||
|
.string()
|
||||||
|
.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(),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export class FollowUserDTO extends createZodDto(FollowUserSchema) {}
|
|
@ -25,12 +25,13 @@ import {
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import { Public } from "src/decorators/public.decorator";
|
import { Public } from "src/decorators/public.decorator";
|
||||||
import { BufferValidator } from "src/validators/buffer.validator";
|
import { BufferValidator } from "src/validators/buffer.validator";
|
||||||
import UploadImageValidator from "src/validators/upload-image.validator";
|
import UploadImageValidator from "src/validators/upload_image.validator";
|
||||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
import { CreateUserDTO } from "./dto/create_user.dto";
|
||||||
import { UpdateEmailDTO } from "./dto/update-email.dto";
|
import { FollowUserDTO } from "./dto/follow_user.dto";
|
||||||
import { UpdateNameDTO } from "./dto/update-name.dto";
|
import { UpdateEmailDTO } from "./dto/update_email.dto";
|
||||||
import { UpdatePasswordDTO } from "./dto/update-password.dto";
|
import { UpdateNameDTO } from "./dto/update_name.dto";
|
||||||
import UploadImageSchema from "./schemas/upload-image.schema";
|
import { UpdatePasswordDTO } from "./dto/update_password.dto";
|
||||||
|
import UploadImageSchema from "./schemas/upload_image.schema";
|
||||||
import { UserService } from "./users.service";
|
import { UserService } from "./users.service";
|
||||||
|
|
||||||
@ApiTags("Users")
|
@ApiTags("Users")
|
||||||
|
@ -50,6 +51,15 @@ export class UserController {
|
||||||
return this.userService.create(createUserDTO);
|
return this.userService.create(createUserDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("/follow")
|
||||||
|
@ApiOperation({ summary: "Follow/unfollow a user" })
|
||||||
|
@ApiCreatedResponse({ description: "Followed/unfollowed successfully" })
|
||||||
|
@ApiNotFoundResponse({ description: "User to follow not found" })
|
||||||
|
@ApiBearerAuth("JWT")
|
||||||
|
follow(@Body() { username }: FollowUserDTO, @Request() req) {
|
||||||
|
return this.userService.follow(req.user.id, username);
|
||||||
|
}
|
||||||
|
|
||||||
// GET
|
// GET
|
||||||
@Get("/profile")
|
@Get("/profile")
|
||||||
@ApiOperation({ summary: "Returns information about the logged user" })
|
@ApiOperation({ summary: "Returns information about the logged user" })
|
||||||
|
|
|
@ -4,10 +4,10 @@ import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as argon2 from "argon2";
|
||||||
import { PrismaService } from "src/services/prisma/prisma.service";
|
import { PrismaService } from "src/services/prisma/prisma.service";
|
||||||
import { S3Service } from "src/services/s3/s3.service";
|
import { S3Service } from "src/services/s3/s3.service";
|
||||||
import { CreateUserDTO } from "./dto/create-user.dto";
|
import { CreateUserDTO } from "./dto/create_user.dto";
|
||||||
import { UserModel } from "./models/user.model";
|
import { UserModel } from "./models/user.model";
|
||||||
import { User } from "./types/user.type";
|
import { User } from "./types/user.type";
|
||||||
|
|
||||||
|
@ -88,8 +88,7 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password encryption
|
// Password encryption
|
||||||
const salt = await bcrypt.genSalt(15);
|
const hash = await argon2.hash(password);
|
||||||
const hash = await bcrypt.hash(password, salt);
|
|
||||||
|
|
||||||
const user = await this.prisma.user.create({
|
const user = await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
@ -107,6 +106,40 @@ export class UserService {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async follow(authenticated_id: string, username: string) {
|
||||||
|
const user_to_follow = await this.prisma.user.findFirst({
|
||||||
|
where: { username },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user_to_follow === null) {
|
||||||
|
throw new NotFoundException("User to follow not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const is_already_following = await this.prisma.follows.findFirst({
|
||||||
|
where: {
|
||||||
|
followerId: user_to_follow.id,
|
||||||
|
followingId: authenticated_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is_already_following !== null) {
|
||||||
|
await this.prisma.follows.deleteMany({
|
||||||
|
where: {
|
||||||
|
followerId: user_to_follow.id,
|
||||||
|
followingId: authenticated_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.prisma.follows.create({
|
||||||
|
data: {
|
||||||
|
followerId: user_to_follow.id,
|
||||||
|
followingId: authenticated_id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateEmail(id: string, email: string): Promise<{ message: string }> {
|
async updateEmail(id: string, email: string): Promise<{ message: string }> {
|
||||||
const user = await this.prisma.user.findFirst({
|
const user = await this.prisma.user.findFirst({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
@ -175,14 +208,13 @@ export class UserService {
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
|
||||||
const validatePassword = await bcrypt.compare(old_password, user.password);
|
const validatePassword = await argon2.verify(user.password, old_password);
|
||||||
|
|
||||||
if (!validatePassword) {
|
if (!validatePassword) {
|
||||||
throw new BadRequestException("Wrong password");
|
throw new BadRequestException("Wrong password");
|
||||||
}
|
}
|
||||||
|
|
||||||
const salt = await bcrypt.genSalt(15);
|
const hash = await argon2.hash(new_password);
|
||||||
const hash = await bcrypt.hash(new_password, salt);
|
|
||||||
|
|
||||||
await this.prisma.user.update({
|
await this.prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -197,7 +229,7 @@ export class UserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImage(id: string, image: File) {
|
async uploadImage(id: string, image: File) {
|
||||||
const url = await this.s3.uploadImageToMinio(id, image.buffer);
|
const url = await this.s3.uploadImage(id, image.buffer);
|
||||||
|
|
||||||
return await this.prisma.user.update({
|
return await this.prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -9,7 +9,7 @@ export class BufferValidator implements PipeTransform {
|
||||||
async transform(value: File) {
|
async transform(value: File) {
|
||||||
const { fileTypeFromBuffer } = await (eval(
|
const { fileTypeFromBuffer } = await (eval(
|
||||||
'import("file-type")',
|
'import("file-type")',
|
||||||
) as Promise<typeof import("file-type")>); // TODO: Find a way to remove this eval. This is very dangerous. TOP PRIORITY. <-- Downgrade to 16.5.4 should work.
|
) as Promise<typeof import("file-type")>);
|
||||||
|
|
||||||
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
const ALLOWED_MIMES = ["image/jpeg", "image/png", "image/webp"];
|
||||||
const buffer_type = await fileTypeFromBuffer(value.buffer);
|
const buffer_type = await fileTypeFromBuffer(value.buffer);
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { INestApplication } from "@nestjs/common";
|
|
||||||
import { Test, TestingModule } from "@nestjs/testing";
|
|
||||||
import * as request from "supertest";
|
|
||||||
import { AppModule } from "./../src/app.module";
|
|
||||||
|
|
||||||
describe("AppController (e2e)", () => {
|
|
||||||
let app: INestApplication;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
||||||
imports: [AppModule],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
|
||||||
await app.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("/ (GET)", () => {
|
|
||||||
return request(app.getHttpServer())
|
|
||||||
.get("/")
|
|
||||||
.expect(200)
|
|
||||||
.expect("Hello World!");
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"moduleFileExtensions": ["js", "json", "ts"],
|
|
||||||
"rootDir": ".",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"testRegex": ".e2e-spec.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue