diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1ed81da..02ded4c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,7 +35,7 @@ model UserMessage { model Class { id String @id @default(cuid()) - name String + name String @unique createdAt DateTime @default(now()) ClassRoom Room[] @@ -96,4 +96,4 @@ model RoomSurveyAnswerUser { enum Role { STUDENT ADMIN -} \ No newline at end of file +} diff --git a/src/ApiResponses/UnauthorizedResponse.ts b/src/ApiResponses/UnauthorizedResponse.ts new file mode 100644 index 0000000..27d9d94 --- /dev/null +++ b/src/ApiResponses/UnauthorizedResponse.ts @@ -0,0 +1,9 @@ +import { ApiResponseNoStatusOptions } from "@nestjs/swagger"; + +export const UnauthorizedResponse = { + description: "Unauthorized", + example: { + message: "Unauthorized", + statusCode: 401, + }, +} as ApiResponseNoStatusOptions; diff --git a/src/modules/auth/guards/role.guard.ts b/src/modules/auth/guards/role.guard.ts index ae4f913..6eec5f4 100644 --- a/src/modules/auth/guards/role.guard.ts +++ b/src/modules/auth/guards/role.guard.ts @@ -13,29 +13,33 @@ export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {} canActivate(context: ExecutionContext): boolean { - const Roles = this.reflector.get( + const RolesHandler = this.reflector.get( "roles", context.getHandler(), ); - if (!Roles) { - return true; - } + const RolesClass = this.reflector.get( + "roles", + context.getClass(), + ); + + if (!RolesHandler && !RolesClass) return true; const request = context.switchToHttp().getRequest() as Request; const user = request.user; - if (!user) { - throw new ForbiddenException("User not authenticated"); - } + if (!user) throw new ForbiddenException("User not authenticated"); - const hasRole = Roles.some((role) => user.role?.includes(role)); + const hasRoleHandler = + RolesHandler?.some((role) => user.role?.includes(role)) ?? + false, + hasRoleClass = + RolesClass?.some((role) => user.role?.includes(role)) ?? false; - if (!hasRole) { + if (hasRoleHandler) return true; + else if (hasRoleClass) return true; + else throw new UnauthorizedException( - `You need to have the role ${Roles.map((role) => role).join(" or ")}`, + `User doesn't have the right role, expected: ${RolesHandler ?? RolesClass}`, ); - } - - return true; } } diff --git a/src/modules/auth/strategy/jwt.strategy.ts b/src/modules/auth/strategy/jwt.strategy.ts index 7a2bef5..f98587e 100644 --- a/src/modules/auth/strategy/jwt.strategy.ts +++ b/src/modules/auth/strategy/jwt.strategy.ts @@ -20,7 +20,7 @@ export class JWTStrategy extends PassportStrategy(Strategy, "jwt") { } async validate(payload: JwtPayload): Promise { - const user = await this.userService.findById(payload.id); + const user = await this.userService.findOne(payload.id); if (!user) { throw new UnauthorizedException("User not found"); diff --git a/src/modules/auth/strategy/oauth2strategy.ts b/src/modules/auth/strategy/oauth2strategy.ts index 38424c7..8baadf4 100644 --- a/src/modules/auth/strategy/oauth2strategy.ts +++ b/src/modules/auth/strategy/oauth2strategy.ts @@ -56,9 +56,7 @@ export class Oauth2Strategy extends PassportStrategy(Strategy, "oauth2") { _refreshToken: string, profile: Oauth2Profile, ): Promise<{ accessToken: string; refreshToken: string }> { - const user = await this.userSerivce.findOne({ - providerId: profile.sub ?? profile.id, - }); + const user = await this.userSerivce.findByProviderId(profile.sub ?? profile.id); if (!user) throw new UnauthorizedException("User not found"); diff --git a/src/modules/auth/strategy/refresh.strategy.ts b/src/modules/auth/strategy/refresh.strategy.ts index e37f79b..6f62ab4 100644 --- a/src/modules/auth/strategy/refresh.strategy.ts +++ b/src/modules/auth/strategy/refresh.strategy.ts @@ -20,7 +20,7 @@ export class RefreshJWTStrategy extends PassportStrategy(Strategy, "refresh") { } async validate(payload: JwtPayload): Promise { - const user = await this.userService.findById(payload.id); + const user = await this.userService.findOne(payload.id); if (!user) { throw new UnauthorizedException("User not found"); diff --git a/src/modules/class/ApiResponses/ClassResponse.ts b/src/modules/class/ApiResponses/ClassResponse.ts new file mode 100644 index 0000000..eecfee3 --- /dev/null +++ b/src/modules/class/ApiResponses/ClassResponse.ts @@ -0,0 +1,40 @@ +import { ApiResponseNoStatusOptions } from "@nestjs/swagger"; +import { ClassEntity } from "../entities/class.entity"; + +export const ClassResponse = { + type: ClassEntity, + examples: { + example: { + summary: "A class", + value: { + id: "1", + name: "Sigyn", + createdAt: new Date(), + } as ClassEntity, + }, + }, +} as ApiResponseNoStatusOptions; + +export const ClassesResponse = { + type: ClassEntity, + isArray: true, + examples: { + example: { + summary: "A list of classes", + value: [ + { id: "1", name: "Sigyn", createdAt: new Date() }, + { id: "2", name: "Loki", createdAt: new Date() }, + ] as ClassEntity[], + }, + }, +} as ApiResponseNoStatusOptions; + +export const ClassCountResponse = { + description: "The class count", + examples: { + example: { + summary: "A count of classes", + value: { count: 2 }, + }, + }, +} as ApiResponseNoStatusOptions; \ No newline at end of file diff --git a/src/modules/class/class.controller.ts b/src/modules/class/class.controller.ts index 8ae4133..2985c80 100644 --- a/src/modules/class/class.controller.ts +++ b/src/modules/class/class.controller.ts @@ -1,42 +1,95 @@ +import { UnauthorizedResponse } from "@/ApiResponses/UnauthorizedResponse"; import { - Controller, - Get, - Post, Body, - Patch, - Param, + Controller, Delete, + Get, + Param, + Patch, + Post, + Query, + UseGuards, } from "@nestjs/common"; +import { + ClassCountResponse, + ClassesResponse, + ClassResponse, +} from "./ApiResponses/ClassResponse"; import { ClassService } from "./class.service"; + import { CreateClassDto } from "./dto/create-class.dto"; import { UpdateClassDto } from "./dto/update-class.dto"; +import { Roles } from "@/modules/auth/decorators/roles.decorator"; +import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard"; +import { RolesGuard } from "@/modules/auth/guards/role.guard"; +import { + ApiBearerAuth, + ApiOkResponse, + ApiQuery, + ApiUnauthorizedResponse, +} from "@nestjs/swagger"; +import { ClassEntity } from "./entities/class.entity"; + @Controller("class") +@UseGuards(RolesGuard) +@UseGuards(JwtAuthGuard) +@ApiBearerAuth() +@Roles(["ADMIN"]) +@ApiUnauthorizedResponse(UnauthorizedResponse) export class ClassController { constructor(private readonly classService: ClassService) {} @Post() - create(@Body() createClassDto: CreateClassDto) { - return this.classService.create(createClassDto); + @ApiOkResponse(ClassResponse) + async create(@Body() createClassDto: CreateClassDto) { + return await this.classService.create(createClassDto).then((class_) => { + return new ClassEntity(class_); + }); } @Get() - findAll() { - return this.classService.findAll(); + @ApiOkResponse(ClassesResponse) + async findAll() { + return await this.classService + .findAll({}) + .then((classes) => + classes.map((class_) => new ClassEntity(class_)), + ); } @Get(":id") - findOne(@Param("id") id: string) { - return this.classService.findOne(+id); + @ApiOkResponse(ClassResponse) + async findOne(@Param("id") id: string) { + return await this.classService + .findOne(id) + .then((class_) => new ClassEntity(class_)); } @Patch(":id") - update(@Param("id") id: string, @Body() updateClassDto: UpdateClassDto) { - return this.classService.update(+id, updateClassDto); + @ApiOkResponse(ClassResponse) + async update( + @Param("id") id: string, + @Body() updateClassDto: UpdateClassDto, + ) { + return await this.classService + .update(id, updateClassDto) + .then((class_) => new ClassEntity(class_)); } @Delete(":id") - remove(@Param("id") id: string) { - return this.classService.remove(+id); + @ApiOkResponse(ClassResponse) + async remove(@Param("id") id: string) { + return await this.classService + .remove(id) + .then((class_) => new ClassEntity(class_)); + } + + @Delete() + @ApiOkResponse(ClassCountResponse) + @ApiQuery({ name: "ids", required: true, type: [String] }) + async bulkRemove(@Query("ids") ids: string | string[]) { + if (typeof ids === "string") ids = [ids]; + return await this.classService.bulkRemove(ids); } } diff --git a/src/modules/class/class.service.ts b/src/modules/class/class.service.ts index b2e33f4..78fdb60 100644 --- a/src/modules/class/class.service.ts +++ b/src/modules/class/class.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { CreateClassDto } from "./dto/create-class.dto"; import { UpdateClassDto } from "./dto/update-class.dto"; import { PrismaService } from "nestjs-prisma"; +import { Prisma } from "@prisma/client"; @Injectable() export class ClassService { @@ -11,12 +12,30 @@ export class ClassService { return await this.prisma.class.create({ data: createClassDto }); } - async findAll({ name }: { name: string }) { - return await this.prisma.class.create({ data: { name } }); + async findAll({ + skip, + take, + cursor, + where, + orderBy, + }: { + skip?: number; + take?: number; + cursor?: Prisma.ClassWhereUniqueInput; + where?: Prisma.ClassWhereInput; + orderBy?: Record; + }) { + return await this.prisma.class.findMany({ + skip, + take, + cursor, + where, + orderBy, + }); } async findOne(id: string) { - return await this.prisma.class.findUnique({ where: { id } }); + return await this.prisma.class.findUniqueOrThrow({ where: { id } }); } async update(id: string, updateClassDto: UpdateClassDto) { @@ -29,4 +48,10 @@ export class ClassService { async remove(id: string) { return await this.prisma.class.delete({ where: { id } }); } + + async bulkRemove(ids: string[]) { + return await this.prisma.class.deleteMany({ + where: { id: { in: ids } }, + }); + } } diff --git a/src/modules/class/dto/create-class.dto.ts b/src/modules/class/dto/create-class.dto.ts index 00e3628..d1389e6 100644 --- a/src/modules/class/dto/create-class.dto.ts +++ b/src/modules/class/dto/create-class.dto.ts @@ -1,3 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; + export class CreateClassDto { + @IsString() + @ApiProperty() name: string; } diff --git a/src/modules/user/ApiResponses/UserReponse.ts b/src/modules/user/ApiResponses/UserReponse.ts new file mode 100644 index 0000000..3d75947 --- /dev/null +++ b/src/modules/user/ApiResponses/UserReponse.ts @@ -0,0 +1,41 @@ +import { ApiResponseNoStatusOptions } from "@nestjs/swagger"; +import { UserEntity } from "../entities/user.entity"; + +export const UserResponse = { + type: UserEntity, + description: "The user has been successfully found.", + examples: { + example: { + summary: "A user example", + value: { + id: "1", + role: "ADMIN", + username: "admin", + } as UserEntity, + }, + }, +} as ApiResponseNoStatusOptions; + +export const UsersResponse = { + type: UserEntity, + isArray: true, + examples: { + example: { + summary: "A list of users", + value: [ + { id: "1", role: "ADMIN", username: "admin" }, + { id: "2", role: "STUDENT", username: "student" }, + ] as UserEntity[], + }, + }, +} as ApiResponseNoStatusOptions; + +export const UserCountResponse = { + description: "The users count", + examples: { + example: { + summary: "A count of users", + value: { count: 2 }, + }, + }, +} as ApiResponseNoStatusOptions; \ No newline at end of file diff --git a/src/modules/user/dto/bulk-delete-user.dto.ts b/src/modules/user/dto/bulk-delete-user.dto.ts deleted file mode 100644 index 9208612..0000000 --- a/src/modules/user/dto/bulk-delete-user.dto.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { ArrayMaxSize, ArrayMinSize, IsArray, IsString } from "class-validator"; - -export class BulkDeleteUserDTO { - @IsArray() - @IsString({ each: true }) - @ArrayMinSize(1) - @ArrayMaxSize(10) - @ApiProperty() - ids: string[]; -} diff --git a/src/modules/user/dto/update-user.dto.ts b/src/modules/user/dto/update-user.dto.ts index 0829883..13f3a37 100644 --- a/src/modules/user/dto/update-user.dto.ts +++ b/src/modules/user/dto/update-user.dto.ts @@ -1,12 +1,5 @@ import { PartialType } from "@nestjs/mapped-types"; -import { IsString } from "class-validator"; import { CreateUserDTO } from "./create-user.dto"; -export class UpdateUserDTO extends PartialType(CreateUserDTO) { - @IsString() - id: string; - - @IsString() - username: string; -} +export class UpdateUserDTO extends PartialType(CreateUserDTO) {} diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index 448e7cb..ca8efb0 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -1,167 +1,87 @@ +import { UnauthorizedResponse } from "@/ApiResponses/UnauthorizedResponse"; +import { Roles } from "@/modules/auth/decorators/roles.decorator"; +import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard"; +import { RolesGuard } from "@/modules/auth/guards/role.guard"; import { Body, Controller, Delete, Get, - Post, + Param, + Patch, Query, - UseGuards, + UseGuards } from "@nestjs/common"; import { ApiBearerAuth, - ApiBody, ApiOkResponse, + ApiParam, ApiQuery, - ApiUnauthorizedResponse, + ApiUnauthorizedResponse } from "@nestjs/swagger"; - -import { Roles } from "@/modules/auth/decorators/roles.decorator"; -import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard"; -import { RolesGuard } from "@/modules/auth/guards/role.guard"; - -import { BulkDeleteUserDTO } from "./dto/bulk-delete-user.dto"; -import { CreateUserDTO } from "./dto/create-user.dto"; - +import { + UserCountResponse, + UserResponse, + UsersResponse, +} from "./ApiResponses/UserReponse"; +import { UpdateUserDTO } from "./dto/update-user.dto"; import { UserEntity } from "./entities/user.entity"; import { UserService } from "./user.service"; -@Controller() +@Controller("user") @UseGuards(RolesGuard) @UseGuards(JwtAuthGuard) @ApiBearerAuth() -@ApiUnauthorizedResponse({ - description: "Unauthorized", - example: { - message: "Unauthorized", - statusCode: 401, - }, -}) +@Roles(["ADMIN"]) +@ApiUnauthorizedResponse(UnauthorizedResponse) export class UserController { constructor(private readonly userService: UserService) {} - @Get("users") - @Roles(["ADMIN"]) - @ApiOkResponse({ - type: UserEntity, - isArray: true, - examples: { - example: { - summary: "A list of users", - value: [ - { id: "1", role: "ADMIN", username: "admin" }, - { id: "2", role: "STUDENT", username: "student" }, - ] as UserEntity[], - }, - }, - }) + @Get() + @ApiOkResponse(UsersResponse) async findAll(): Promise { return await this.userService .findAll() .then((users) => users.map((user) => new UserEntity(user))); } - @Get("user") - @Roles(["ADMIN"]) - @ApiOkResponse({ - type: UserEntity, - description: "The user has been successfully found.", - examples: { - example: { - summary: "A user example", - value: { - id: "1", - role: "ADMIN", - username: "admin", - } as UserEntity, - }, - }, - }) - async findById(@Query("id") id: string): Promise { + @Get(":id") + @ApiOkResponse(UserResponse) + async findOne(@Param("id") id: string): Promise { return this.userService - .findById(id) + .findOne(id) .then((user) => new UserEntity(user)); } - @Post("user") - @Roles(["ADMIN"]) - @ApiOkResponse({ - type: UserEntity, - description: "The user has been successfully created.", - examples: { - example: { - summary: "A user example", - value: { - id: "1", - role: "ADMIN", - username: "admin", - } as UserEntity, - }, - }, - }) - @ApiBody({ - type: CreateUserDTO, - examples: { - example: { - summary: "A user example", - value: { username: "admin" }, - }, - }, - }) - async create(@Body() createUserInput: CreateUserDTO): Promise { + @Patch(":id") + async update( + @Param("id") id: string, + @Body() updateUserDto: UpdateUserDTO, + ): Promise { return this.userService - .create(createUserInput) + .update(id, updateUserDto) .then((user) => new UserEntity(user)); } - @Delete("user") - @Roles(["ADMIN"]) - @ApiOkResponse({ - type: UserEntity, - description: "The user has been successfully deleted.", - examples: { - example: { - summary: "A user example", - value: { - id: "1", - role: "ADMIN", - username: "admin", - } as UserEntity, - }, - }, - }) - @ApiQuery({ + @Delete(":id") + @ApiOkResponse(UserResponse) + @ApiParam({ name: "id", type: String, description: "The user id", example: "1", }) - async delete(@Query("id") id: string): Promise { - return this.userService.delete(id).then((user) => new UserEntity(user)); + async remove(@Param("id") id: string): Promise { + return this.userService.remove(id).then((user) => new UserEntity(user)); } - @Delete("users") - @Roles(["ADMIN"]) - @ApiOkResponse({ - description: "The users have been successfully deleted.", - examples: { - example: { - summary: "A count of deleted users", - value: { count: 2 }, - }, - }, - }) - @ApiBody({ - type: BulkDeleteUserDTO, - examples: { - example: { - summary: "A list of user ids", - value: { ids: ["1", "2"] }, - }, - }, - }) - bulkDelete(@Body() { ids }: BulkDeleteUserDTO): Promise<{ + @Delete() + @ApiOkResponse(UserCountResponse) + @ApiQuery({ name: "ids", required: true, type: [String] }) + bulkRemove(@Query("ids") ids: string | string[]): Promise<{ count: number; }> { - return this.userService.bulkDelete(ids); + if (typeof ids === "string") ids = [ids]; + return this.userService.bulkRemove(ids); } } diff --git a/src/modules/user/user.gateway.ts b/src/modules/user/user.gateway.ts index da128a0..a3823ce 100644 --- a/src/modules/user/user.gateway.ts +++ b/src/modules/user/user.gateway.ts @@ -31,7 +31,7 @@ export class UserGateway implements OnGatewayConnection, OnGatewayDisconnect { return client.disconnect(); } - const user = await this.userService.findById(jwtDecoded.id); + const user = await this.userService.findOne(jwtDecoded.id); if (!user) { client.emit("auth", "User not found"); diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 3498586..1531787 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { PrismaService } from "nestjs-prisma"; @@ -10,61 +10,66 @@ export class UserService { constructor(private readonly prisma: PrismaService) {} async findAll({ - include, - cursor, + skip, take, + cursor, + where, + orderBy, }: { - include?: Prisma.UserInclude; - cursor?: string; + skip?: number; take?: number; + cursor?: Prisma.UserWhereUniqueInput; + where?: Prisma.UserWhereInput; + orderBy?: Record; } = {}) { return await this.prisma.user.findMany({ - include, - cursor: cursor ? { id: cursor } : undefined, - take: take || 10, + skip, + take, + cursor, + where, + orderBy, }); } - async findById(id: string = null) { - return await this.prisma.user.findUnique({ + async findOne(id: string) { + return await this.prisma.user.findUniqueOrThrow({ where: { id }, }); } - async findOne(where: Prisma.UserWhereInput) { - return await this.prisma.user.findFirst({ - where, - }); - } - - async create(createUserInput: CreateUserDTO) { - return await this.prisma.user.create({ - data: { - username: createUserInput.username, - providerId: createUserInput.providerId, + async findByProviderId(providerId: string) { + return await this.prisma.user.findUniqueOrThrow({ + where: { + providerId, }, }); } - async update(updateUserInput: UpdateUserDTO) { + async create(createUserDto: CreateUserDTO) { + return await this.prisma.user.create({ + data: { + username: createUserDto.username, + providerId: createUserDto.providerId, + }, + }); + } + + async update(id: string, updateUserInput: UpdateUserDTO) { return await this.prisma.user.update({ - where: { id: updateUserInput.id }, + where: { id }, data: { username: updateUserInput.username, }, }); } - async delete(id: string) { - const exist = await this.prisma.user.findUnique({ where: { id } }); - if (!exist) throw new NotFoundException("User not found"); - + async remove(id: string) { return await this.prisma.user.delete({ where: { id }, }); } - async bulkDelete(ids: string[]) { + async bulkRemove(ids: string[]) { return await this.prisma.user.deleteMany({ where: { id: {