diff --git a/prisma/migrations/20240126133535_renamed_posts/migration.sql b/prisma/migrations/20240126133535_renamed_posts/migration.sql new file mode 100644 index 0000000..9a1a82b --- /dev/null +++ b/prisma/migrations/20240126133535_renamed_posts/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3d63358..7199333 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,37 +13,37 @@ model User { username String @unique email String @unique password String - posts Post[] + kweeks Kweek[] profileImage String? - likedPosts PostLike[] + likedKweeks KweekLike[] likedComments CommentLike[] followers Follows[] @relation("follower") following Follows[] @relation("following") - postComments Comments[] + kweeksComments Comments[] fromNotifications Notifications[] @relation("fromNotifications") toNotifications Notifications[] @relation("toNotifications") socketId String? createdAt DateTime @default(now()) } -model Post { +model Kweek { id String @id @default(uuid()) content String authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) - likes PostLike[] + likes KweekLike[] comments Comments[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } -model PostLike { - id String @id @default(uuid()) - postId String - post Post @relation(fields: [postId], references: [id], onDelete: Cascade) - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - createdAt DateTime @default(now()) +model KweekLike { + id String @id @default(uuid()) + kweekId String + kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) } // I should join these two up? Yeah, but I will not do it since it didn't work on the first time. @@ -71,8 +71,8 @@ model Comments { content String userId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) - postId String - post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + kweekId String + kweek Kweek @relation(fields: [kweekId], references: [id], onDelete: Cascade) likes CommentLike[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) diff --git a/src/app.module.ts b/src/app.module.ts index b3e06f1..02cb120 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,18 +1,17 @@ import { Module } from "@nestjs/common"; -import { UserModule } from "./user/user.module"; +import { UserModule } from "./users/users.module"; import { APP_GUARD, APP_PIPE } from "@nestjs/core"; import { ZodValidationPipe } from "nestjs-zod"; -import { PostModule } from "./post/post.module"; import { AuthModule } from "./auth/auth.module"; import { ConfigModule } from "@nestjs/config"; import { JwtAuthGuard } from "./auth/jwt-auth.guard"; import { ThrottlerGuard, ThrottlerModule } from "@nestjs/throttler"; import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; +import { KweeksModule } from './kweeks/kweeks.module'; @Module({ imports: [ UserModule, - PostModule, AuthModule, ConfigModule.forRoot({ isGlobal: true, @@ -23,6 +22,7 @@ import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis"; `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`, ), }), + KweeksModule, ], providers: [ { diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 742b84c..8a9115a 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -24,7 +24,7 @@ export class AuthController { @Public() @UseGuards(LocalAuthGuard) - @Post("/login") + @Post("/") @ApiOperation({ summary: "Authenticates a user" }) @ApiOkResponse({ status: 200, description: "Authenticated successfully" }) @ApiUnauthorizedResponse({ description: "Wrong username or password" }) diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 483d59a..6d1e5eb 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -2,7 +2,7 @@ import { Module } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { PassportModule } from "@nestjs/passport"; import { LocalStrategy } from "./local.strategy"; -import { UserModule } from "src/user/user.module"; +import { UserModule } from "src/users/users.module"; import { AuthController } from "./auth.controller"; import { JwtModule } from "@nestjs/jwt"; import { JwtStrategy } from "./jwt.strategy"; diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index e4824d3..c52ee07 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,7 +1,7 @@ import { Injectable } from "@nestjs/common"; -import { UserService } from "src/user/user.service"; +import { UserService } from "src/users/users.service"; import * as bcrypt from "bcrypt"; -import { UserModel } from "src/user/models/user.model"; +import { UserModel } from "src/users/models/user.model"; import { JwtService } from "@nestjs/jwt"; @Injectable() @@ -15,7 +15,7 @@ export class AuthService { username: string, password: string, ): Promise { - const user = await this.userService.search(username); + const user = await this.userService.auth_search(username); if (user === undefined) { return null; diff --git a/src/auth/local.strategy.ts b/src/auth/local.strategy.ts index 7948877..c2cccf6 100644 --- a/src/auth/local.strategy.ts +++ b/src/auth/local.strategy.ts @@ -2,7 +2,7 @@ import { Injectable, UnauthorizedException } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Strategy } from "passport-local"; import { AuthService } from "./auth.service"; -import { UserModel } from "src/user/models/user.model"; +import { UserModel } from "src/users/models/user.model"; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { diff --git a/src/kweeks/dto/create-kweek.dto.ts b/src/kweeks/dto/create-kweek.dto.ts new file mode 100644 index 0000000..fe28f7e --- /dev/null +++ b/src/kweeks/dto/create-kweek.dto.ts @@ -0,0 +1 @@ +export class CreateKweekDto {} diff --git a/src/kweeks/dto/update-kweek.dto.ts b/src/kweeks/dto/update-kweek.dto.ts new file mode 100644 index 0000000..1b58478 --- /dev/null +++ b/src/kweeks/dto/update-kweek.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/swagger'; +import { CreateKweekDto } from './create-kweek.dto'; + +export class UpdateKweekDto extends PartialType(CreateKweekDto) {} diff --git a/src/kweeks/entities/kweek.entity.ts b/src/kweeks/entities/kweek.entity.ts new file mode 100644 index 0000000..a261213 --- /dev/null +++ b/src/kweeks/entities/kweek.entity.ts @@ -0,0 +1 @@ +export class Kweek {} diff --git a/src/kweeks/kweeks.controller.spec.ts b/src/kweeks/kweeks.controller.spec.ts new file mode 100644 index 0000000..ccc08f2 --- /dev/null +++ b/src/kweeks/kweeks.controller.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KweeksController } from './kweeks.controller'; +import { KweeksService } from './kweeks.service'; + +describe('KweeksController', () => { + let controller: KweeksController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [KweeksController], + providers: [KweeksService], + }).compile(); + + controller = module.get(KweeksController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/kweeks/kweeks.controller.ts b/src/kweeks/kweeks.controller.ts new file mode 100644 index 0000000..b32ad4c --- /dev/null +++ b/src/kweeks/kweeks.controller.ts @@ -0,0 +1,78 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Param, + Delete, +} from "@nestjs/common"; +import { KweeksService } from "./kweeks.service"; +import { CreateKweekDto } from "./dto/create-kweek.dto"; +import { UpdateKweekDto } from "./dto/update-kweek.dto"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { Public } from "src/public.decorator"; + +@ApiTags("Kweeks") +@Controller("kweeks") +export class KweeksController { + constructor(private readonly kweeksService: KweeksService) {} + + @Post() + @ApiOperation({ summary: "Creates a kweek" }) + @ApiBearerAuth("JWT") + create(@Body() createKweekDto: CreateKweekDto) { + return this.kweeksService.create(createKweekDto); + } + + @Public() + @Get(":id") + @ApiOperation({ summary: "Retrieves information about a kweek" }) + findOne(@Param("id") id: string) { + return this.kweeksService.findOne(+id); + } + + @Patch(":id") + @ApiOperation({ summary: "Updates a kweek content" }) + @ApiBearerAuth("JWT") + update(@Param("id") id: string, @Body() updateKweekDto: UpdateKweekDto) { + return this.kweeksService.update(+id, updateKweekDto); + } + + @Delete(":id") + @ApiOperation({ summary: "Deletes a kweek" }) + @ApiBearerAuth("JWT") + remove(@Param("id") id: string) { + return this.kweeksService.remove(+id); + } + + @Post(":id/like") + @ApiOperation({ summary: "Likes a kweek" }) + @ApiBearerAuth("JWT") + likeKweek() {} + + @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() {} +} diff --git a/src/kweeks/kweeks.module.ts b/src/kweeks/kweeks.module.ts new file mode 100644 index 0000000..6b84b07 --- /dev/null +++ b/src/kweeks/kweeks.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { KweeksService } from './kweeks.service'; +import { KweeksController } from './kweeks.controller'; + +@Module({ + controllers: [KweeksController], + providers: [KweeksService], +}) +export class KweeksModule {} diff --git a/src/kweeks/kweeks.service.spec.ts b/src/kweeks/kweeks.service.spec.ts new file mode 100644 index 0000000..aae88f4 --- /dev/null +++ b/src/kweeks/kweeks.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { KweeksService } from './kweeks.service'; + +describe('KweeksService', () => { + let service: KweeksService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [KweeksService], + }).compile(); + + service = module.get(KweeksService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/kweeks/kweeks.service.ts b/src/kweeks/kweeks.service.ts new file mode 100644 index 0000000..452763b --- /dev/null +++ b/src/kweeks/kweeks.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@nestjs/common'; +import { CreateKweekDto } from './dto/create-kweek.dto'; +import { UpdateKweekDto } from './dto/update-kweek.dto'; + +@Injectable() +export class KweeksService { + create(createKweekDto: CreateKweekDto) { + return 'This action adds a new kweek'; + } + + findAll() { + return `This action returns all kweeks`; + } + + findOne(id: number) { + return `This action returns a #${id} kweek`; + } + + update(id: number, updateKweekDto: UpdateKweekDto) { + return `This action updates a #${id} kweek`; + } + + remove(id: number) { + return `This action removes a #${id} kweek`; + } +} diff --git a/src/main.ts b/src/main.ts index 9d00192..ce28080 100644 --- a/src/main.ts +++ b/src/main.ts @@ -34,9 +34,8 @@ async function bootstrap() { "JWT", ) .addTag("Auth") - .addTag("Comment") - .addTag("Post") - .addTag("User") + .addTag("Kweeks") + .addTag("Users") .build(); const document = SwaggerModule.createDocument(app, config); diff --git a/src/post/dto/create-post.dto.ts b/src/post/dto/create-post.dto.ts deleted file mode 100644 index 1a2b3c5..0000000 --- a/src/post/dto/create-post.dto.ts +++ /dev/null @@ -1 +0,0 @@ -export class CreatePostDto {} diff --git a/src/post/dto/update-post.dto.ts b/src/post/dto/update-post.dto.ts deleted file mode 100644 index 7545cd5..0000000 --- a/src/post/dto/update-post.dto.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PartialType } from '@nestjs/swagger'; -import { CreatePostDto } from './create-post.dto'; - -export class UpdatePostDto extends PartialType(CreatePostDto) {} diff --git a/src/post/entities/post.entity.ts b/src/post/entities/post.entity.ts deleted file mode 100644 index 54fbf02..0000000 --- a/src/post/entities/post.entity.ts +++ /dev/null @@ -1 +0,0 @@ -export class Post {} diff --git a/src/post/post.controller.ts b/src/post/post.controller.ts deleted file mode 100644 index a94eeac..0000000 --- a/src/post/post.controller.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Controller, - Get, - Post, - Body, - Patch, - Param, - Delete, -} from "@nestjs/common"; -import { PostService } from "./post.service"; -import { CreatePostDto } from "./dto/create-post.dto"; -import { UpdatePostDto } from "./dto/update-post.dto"; -import { ApiTags } from "@nestjs/swagger"; - -@ApiTags("Post") -@Controller("post") -export class PostController { - constructor(private readonly postService: PostService) {} - - @Post() - create(@Body() createPostDto: CreatePostDto) { - return this.postService.create(createPostDto); - } - - @Get() - findAll() { - return this.postService.findAll(); - } - - @Get(":id") - findOne(@Param("id") id: string) { - return this.postService.findOne(+id); - } - - @Patch(":id") - update(@Param("id") id: string, @Body() updatePostDto: UpdatePostDto) { - return this.postService.update(+id, updatePostDto); - } - - @Delete(":id") - remove(@Param("id") id: string) { - return this.postService.remove(+id); - } -} diff --git a/src/post/post.module.ts b/src/post/post.module.ts deleted file mode 100644 index a5a9cfe..0000000 --- a/src/post/post.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { PostService } from './post.service'; -import { PostController } from './post.controller'; - -@Module({ - controllers: [PostController], - providers: [PostService], -}) -export class PostModule {} diff --git a/src/post/post.service.ts b/src/post/post.service.ts deleted file mode 100644 index d6a68ca..0000000 --- a/src/post/post.service.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { CreatePostDto } from "./dto/create-post.dto"; -import { UpdatePostDto } from "./dto/update-post.dto"; - -@Injectable() -export class PostService { - create(createPostDto: CreatePostDto) { - return "This action adds a new post"; - } - - findAll() { - return "This action returns all post"; - } - - findOne(id: number) { - return `This action returns a #${id} post`; - } - - update(id: number, updatePostDto: UpdatePostDto) { - return `This action updates a #${id} post`; - } - - remove(id: number) { - return `This action removes a #${id} post`; - } -} diff --git a/src/prisma.service.ts b/src/prisma.service.ts deleted file mode 100644 index 96a9bd6..0000000 --- a/src/prisma.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Injectable, OnModuleInit } from "@nestjs/common"; -import { PrismaClient } from "@prisma/client"; - -@Injectable() -export class PrismaService extends PrismaClient implements OnModuleInit { - async onModuleInit() { - await this.$connect(); - } -} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts new file mode 100644 index 0000000..5671cd6 --- /dev/null +++ b/src/prisma/prisma.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { PrismaService } from "./prisma.service"; + +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts new file mode 100644 index 0000000..9082f78 --- /dev/null +++ b/src/prisma/prisma.service.ts @@ -0,0 +1,18 @@ +import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common"; +import { Prisma, PrismaClient } from "@prisma/client"; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit +{ + async onModuleInit() { + await this.$connect(); + } + + async enableShutdownHooks(app: INestApplication) { + this.$on("beforeExit", async () => { + await app.close(); + }); + } +} diff --git a/src/user/models/user.model.ts b/src/user/models/user.model.ts deleted file mode 100644 index b68d339..0000000 --- a/src/user/models/user.model.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createZodDto } from "nestjs-zod"; -import { z } from "nestjs-zod/z"; - -// TODO: Add posts, liked_posts, liked_comments, followers, following, post_comments and notifications field - -export const UserSchema = z - .object({ - id: z.string().uuid(), - displayName: z.string(), - username: z.string(), - email: z.string().email(), - password: z.password(), - profileImage: z.string().url(), - createdAt: z.date(), - }) - .required(); - -export class UserModel extends createZodDto(UserSchema) {} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts deleted file mode 100644 index 369ec0b..0000000 --- a/src/user/user.controller.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Body, Controller, Get, Post, Request } from "@nestjs/common"; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCreatedResponse, - ApiOperation, - ApiTags, - ApiUnauthorizedResponse, -} from "@nestjs/swagger"; -import { UserService } from "./user.service"; -import { CreateUserDTO } from "./dto/create-user.dto"; -import { Public } from "src/public.decorator"; - -@ApiTags("User") -@Controller("user") -export class UserController { - constructor(private readonly userService: UserService) {} - // GET - @Get("/me") - @ApiOperation({ summary: "Returns information about the logged user" }) - @ApiBearerAuth("JWT") - @ApiUnauthorizedResponse({ - description: "Not authenticated / Invalid JWT Token", - }) - async me(@Request() req) { - return req.user; // TODO: Add typing to req.user - } - - // POST - @Public() - @Post("/signup") - @ApiOperation({ summary: "Creates a new account" }) - @ApiCreatedResponse({ description: "Account created successfully" }) - @ApiBadRequestResponse({ - description: - "Missing field / Invalid username / Invalid email / Weak password", - }) - async create(@Body() createUserDTO: CreateUserDTO) { - return this.userService.create(createUserDTO); - } - - // PUT -} diff --git a/src/user/user.module.ts b/src/user/user.module.ts deleted file mode 100644 index b6a36bd..0000000 --- a/src/user/user.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Module } from "@nestjs/common"; -import { UserController } from "./user.controller"; -import { UserService } from "./user.service"; -import { PrismaService } from "src/prisma.service"; - -@Module({ - controllers: [UserController], - providers: [UserService, PrismaService], - exports: [UserService], -}) -export class UserModule {} diff --git a/src/user/user.service.ts b/src/user/user.service.ts deleted file mode 100644 index 0ed4e76..0000000 --- a/src/user/user.service.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { BadRequestException, Injectable } from "@nestjs/common"; -import { CreateUserDTO } from "./dto/create-user.dto"; -import { PrismaService } from "src/prisma.service"; -import { UserModel } from "./models/user.model"; -import * as bcrypt from "bcrypt"; - -@Injectable() -export class UserService { - constructor(private prisma: PrismaService) {} - - async create({ - username, - email, - password, - }: CreateUserDTO): Promise< - Pick - > { - if ((await this.prisma.user.findFirst({ where: { username } })) != null) { - throw new BadRequestException("Username already in use"); - } - - if ((await this.prisma.user.findFirst({ where: { email } })) != null) { - throw new BadRequestException("Email already in use"); - } - - // Password encryption - const salt = await bcrypt.genSalt(15); - const hash = await bcrypt.hash(password, salt); - - const user = await this.prisma.user.create({ - data: { - username, - email, - password: hash, - }, - select: { - displayName: true, - username: true, - createdAt: true, - }, - }); - - return user; - } - - async search(username: string): Promise { - const user = await this.prisma.user.findFirst({ - where: { - username, - }, - select: { - id: true, - profileImage: true, - displayName: true, - username: true, - password: true, - }, - }); - - if (user == null) { - return undefined; - } - - return user; - } -} diff --git a/src/user/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts similarity index 100% rename from src/user/dto/create-user.dto.ts rename to src/users/dto/create-user.dto.ts diff --git a/src/users/dto/update-name.dto.ts b/src/users/dto/update-name.dto.ts new file mode 100644 index 0000000..1395d0d --- /dev/null +++ b/src/users/dto/update-name.dto.ts @@ -0,0 +1,20 @@ +import { createZodDto } from "nestjs-zod"; +import { z } from "nestjs-zod/z"; + +export const UpdateNameSchema = 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() + .describe("New username - optional") + .optional() + .or(z.literal("")), + displayName: z.string({ required_error: "Display name is required" }), + }) + .required(); + +export class UpdateNameDTO extends createZodDto(UpdateNameSchema) {} diff --git a/src/users/models/user.model.ts b/src/users/models/user.model.ts new file mode 100644 index 0000000..775b521 --- /dev/null +++ b/src/users/models/user.model.ts @@ -0,0 +1,22 @@ +import { createZodDto } from "nestjs-zod"; +import { z } from "nestjs-zod/z"; + +export const UserSchema = z + .object({ + id: z.string().uuid(), + displayName: z.string().optional(), + username: z.string(), + email: z.string().email(), + password: z.password(), + kweeks: z.array(z.object({})).optional(), + profileImage: z.string().url().optional(), + likedKweeks: z.array(z.object({})).optional(), + likedComments: z.array(z.object({})).optional(), + followers: z.number(), + following: z.number(), + kweeksComments: z.array(z.object({})).optional(), + createdAt: z.date(), + }) + .required(); + +export class UserModel extends createZodDto(UserSchema) {} diff --git a/src/users/types/user.type.ts b/src/users/types/user.type.ts new file mode 100644 index 0000000..be1d8d5 --- /dev/null +++ b/src/users/types/user.type.ts @@ -0,0 +1,5 @@ +export type User = { + displayName: string; + username: string; + id: string; +}; diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts new file mode 100644 index 0000000..ac4e3e6 --- /dev/null +++ b/src/users/users.controller.ts @@ -0,0 +1,94 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + Request, +} from "@nestjs/common"; +import { + ApiBadRequestResponse, + ApiBearerAuth, + ApiCreatedResponse, + ApiNotFoundResponse, + ApiOperation, + ApiTags, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; +import { UserService } from "./users.service"; +import { CreateUserDTO } from "./dto/create-user.dto"; +import { Public } from "src/public.decorator"; +import { UpdateNameDTO } from "./dto/update-name.dto"; +import { User } from "./types/user.type"; + +@ApiTags("Users") +@Controller("users") +export class UserController { + constructor(private readonly userService: UserService) {} + // POST + @Public() + @Post() + @ApiOperation({ summary: "Creates a new account" }) + @ApiCreatedResponse({ description: "Account created successfully" }) + @ApiBadRequestResponse({ + description: + "Missing field / Invalid username / Invalid email / Weak password", + }) + create(@Body() createUserDTO: CreateUserDTO) { + return this.userService.create(createUserDTO); + } + + // GET + @Get("/profile") + @ApiOperation({ summary: "Returns information about the logged user" }) + @ApiBearerAuth("JWT") + @ApiUnauthorizedResponse({ + description: "Not authenticated / Invalid JWT Token", + }) + me(@Request() req) { + return req.user; // TODO: Add typing to req.user + } + + @Public() + @Get(":username") + @ApiOperation({ summary: "Returns information about a user" }) + @ApiNotFoundResponse({ description: "User not found" }) + @HttpCode(200) + info(@Param("username") username: string) { + return this.userService.info(username); + } + + // PATCH + @Patch() + @ApiOperation({ + summary: "Updates the username or display name of a logged user", + }) + @ApiBearerAuth("JWT") + updateName(@Body() { displayName, username }: UpdateNameDTO, @Request() req) { + return this.userService.updateName(req.user as User, username, displayName); + } + + @Patch("/email") + @ApiOperation({ summary: "Updates the email of a logged user" }) + @ApiBearerAuth("JWT") + updateEmail() {} + + @Patch("/password") + @ApiOperation({ summary: "Updates the password of a logged user" }) + @ApiBearerAuth("JWT") + updatePassword() {} + + @Patch("/image") + @ApiOperation({ summary: "Add a profile image" }) + @ApiBearerAuth("JWT") + uploadProfileImage() {} + + // DELETE + @Delete() + @ApiOperation({ summary: "Deletes the account of a logged user" }) + @ApiBearerAuth("JWT") + remove() {} +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts new file mode 100644 index 0000000..ea4e5ae --- /dev/null +++ b/src/users/users.module.ts @@ -0,0 +1,12 @@ +import { Module } from "@nestjs/common"; +import { UserController } from "./users.controller"; +import { UserService } from "./users.service"; +import { PrismaModule } from "src/prisma/prisma.module"; + +@Module({ + imports: [PrismaModule], + controllers: [UserController], + providers: [UserService], + exports: [UserService], +}) +export class UserModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts new file mode 100644 index 0000000..9e87f68 --- /dev/null +++ b/src/users/users.service.ts @@ -0,0 +1,136 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import { CreateUserDTO } from "./dto/create-user.dto"; +import { PrismaService } from "src/prisma/prisma.service"; +import { UserModel } from "./models/user.model"; +import * as bcrypt from "bcrypt"; +import { User } from "./types/user.type"; + +@Injectable() +export class UserService { + constructor(private prisma: PrismaService) {} + async auth_search(username: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { + username, + }, + select: { + id: true, + profileImage: true, + displayName: true, + username: true, + password: true, + }, + }); + + if (user == null) { + return undefined; + } + + return user; + } + + async info(username: string): Promise { + const user = await this.prisma.user.findFirst({ + where: { username }, + select: { + id: true, + profileImage: true, + displayName: true, + username: true, + createdAt: true, + followers: true, + following: true, + kweeks: { + select: { + id: true, + content: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + }); + + if (user === null) { + throw new NotFoundException("User not found"); + } + + return { + ...user, + followers: user.followers.length, + following: user.following.length, + }; + } + + async create({ + username, + email, + password, + }: CreateUserDTO): Promise< + Pick + > { + if ((await this.prisma.user.findFirst({ where: { username } })) != null) { + throw new BadRequestException("Username already in use"); + } + + if ((await this.prisma.user.findFirst({ where: { email } })) != null) { + throw new BadRequestException("Email already in use"); + } + + // Password encryption + const salt = await bcrypt.genSalt(15); + const hash = await bcrypt.hash(password, salt); + + const user = await this.prisma.user.create({ + data: { + username, + email, + password: hash, + }, + select: { + displayName: true, + username: true, + createdAt: true, + }, + }); + + return user; + } + + async updateName( + loggedUser: User, + username: string | undefined, + displayName: string, + ): Promise> { + const user = await this.prisma.user.findFirst({ + where: { id: loggedUser.id }, + }); + + if (username !== undefined && username.trim() !== user.username) { + const isAlreadyInUse = await this.prisma.user.findFirst({ + where: { username }, + }); + if (isAlreadyInUse != null && isAlreadyInUse.username !== user.username) { + throw new BadRequestException("Username already in use"); + } + } + + return await this.prisma.user.update({ + where: { + id: loggedUser.id, + }, + data: { + displayName, + username: username ?? user.username, + }, + select: { + displayName: true, + username: true, + }, + }); + } +}