chrore: Integrate Class CRUD & Service

refactor: user CRUD & Service
This commit is contained in:
M1000fr 2024-12-10 14:19:27 +01:00
parent 238b01f5f3
commit 161b01d8cb
16 changed files with 289 additions and 207 deletions

View File

@ -35,7 +35,7 @@ model UserMessage {
model Class { model Class {
id String @id @default(cuid()) id String @id @default(cuid())
name String name String @unique
createdAt DateTime @default(now()) createdAt DateTime @default(now())
ClassRoom Room[] ClassRoom Room[]
@ -96,4 +96,4 @@ model RoomSurveyAnswerUser {
enum Role { enum Role {
STUDENT STUDENT
ADMIN ADMIN
} }

View File

@ -0,0 +1,9 @@
import { ApiResponseNoStatusOptions } from "@nestjs/swagger";
export const UnauthorizedResponse = {
description: "Unauthorized",
example: {
message: "Unauthorized",
statusCode: 401,
},
} as ApiResponseNoStatusOptions;

View File

@ -13,29 +13,33 @@ export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {} constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { canActivate(context: ExecutionContext): boolean {
const Roles = this.reflector.get<string[]>( const RolesHandler = this.reflector.get<string[]>(
"roles", "roles",
context.getHandler(), context.getHandler(),
); );
if (!Roles) { const RolesClass = this.reflector.get<string[]>(
return true; "roles",
} context.getClass(),
);
if (!RolesHandler && !RolesClass) return true;
const request = context.switchToHttp().getRequest() as Request; const request = context.switchToHttp().getRequest() as Request;
const user = request.user; const user = request.user;
if (!user) { if (!user) throw new ForbiddenException("User not authenticated");
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( 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;
} }
} }

View File

@ -20,7 +20,7 @@ export class JWTStrategy extends PassportStrategy(Strategy, "jwt") {
} }
async validate(payload: JwtPayload): Promise<User> { async validate(payload: JwtPayload): Promise<User> {
const user = await this.userService.findById(payload.id); const user = await this.userService.findOne(payload.id);
if (!user) { if (!user) {
throw new UnauthorizedException("User not found"); throw new UnauthorizedException("User not found");

View File

@ -56,9 +56,7 @@ export class Oauth2Strategy extends PassportStrategy(Strategy, "oauth2") {
_refreshToken: string, _refreshToken: string,
profile: Oauth2Profile, profile: Oauth2Profile,
): Promise<{ accessToken: string; refreshToken: string }> { ): Promise<{ accessToken: string; refreshToken: string }> {
const user = await this.userSerivce.findOne({ const user = await this.userSerivce.findByProviderId(profile.sub ?? profile.id);
providerId: profile.sub ?? profile.id,
});
if (!user) throw new UnauthorizedException("User not found"); if (!user) throw new UnauthorizedException("User not found");

View File

@ -20,7 +20,7 @@ export class RefreshJWTStrategy extends PassportStrategy(Strategy, "refresh") {
} }
async validate(payload: JwtPayload): Promise<User> { async validate(payload: JwtPayload): Promise<User> {
const user = await this.userService.findById(payload.id); const user = await this.userService.findOne(payload.id);
if (!user) { if (!user) {
throw new UnauthorizedException("User not found"); throw new UnauthorizedException("User not found");

View File

@ -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;

View File

@ -1,42 +1,95 @@
import { UnauthorizedResponse } from "@/ApiResponses/UnauthorizedResponse";
import { import {
Controller,
Get,
Post,
Body, Body,
Patch, Controller,
Param,
Delete, Delete,
Get,
Param,
Patch,
Post,
Query,
UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import {
ClassCountResponse,
ClassesResponse,
ClassResponse,
} from "./ApiResponses/ClassResponse";
import { ClassService } from "./class.service"; import { ClassService } from "./class.service";
import { CreateClassDto } from "./dto/create-class.dto"; import { CreateClassDto } from "./dto/create-class.dto";
import { UpdateClassDto } from "./dto/update-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") @Controller("class")
@UseGuards(RolesGuard)
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@Roles(["ADMIN"])
@ApiUnauthorizedResponse(UnauthorizedResponse)
export class ClassController { export class ClassController {
constructor(private readonly classService: ClassService) {} constructor(private readonly classService: ClassService) {}
@Post() @Post()
create(@Body() createClassDto: CreateClassDto) { @ApiOkResponse(ClassResponse)
return this.classService.create(createClassDto); async create(@Body() createClassDto: CreateClassDto) {
return await this.classService.create(createClassDto).then((class_) => {
return new ClassEntity(class_);
});
} }
@Get() @Get()
findAll() { @ApiOkResponse(ClassesResponse)
return this.classService.findAll(); async findAll() {
return await this.classService
.findAll({})
.then((classes) =>
classes.map((class_) => new ClassEntity(class_)),
);
} }
@Get(":id") @Get(":id")
findOne(@Param("id") id: string) { @ApiOkResponse(ClassResponse)
return this.classService.findOne(+id); async findOne(@Param("id") id: string) {
return await this.classService
.findOne(id)
.then((class_) => new ClassEntity(class_));
} }
@Patch(":id") @Patch(":id")
update(@Param("id") id: string, @Body() updateClassDto: UpdateClassDto) { @ApiOkResponse(ClassResponse)
return this.classService.update(+id, updateClassDto); async update(
@Param("id") id: string,
@Body() updateClassDto: UpdateClassDto,
) {
return await this.classService
.update(id, updateClassDto)
.then((class_) => new ClassEntity(class_));
} }
@Delete(":id") @Delete(":id")
remove(@Param("id") id: string) { @ApiOkResponse(ClassResponse)
return this.classService.remove(+id); 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);
} }
} }

View File

@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common";
import { CreateClassDto } from "./dto/create-class.dto"; import { CreateClassDto } from "./dto/create-class.dto";
import { UpdateClassDto } from "./dto/update-class.dto"; import { UpdateClassDto } from "./dto/update-class.dto";
import { PrismaService } from "nestjs-prisma"; import { PrismaService } from "nestjs-prisma";
import { Prisma } from "@prisma/client";
@Injectable() @Injectable()
export class ClassService { export class ClassService {
@ -11,12 +12,30 @@ export class ClassService {
return await this.prisma.class.create({ data: createClassDto }); return await this.prisma.class.create({ data: createClassDto });
} }
async findAll({ name }: { name: string }) { async findAll({
return await this.prisma.class.create({ data: { name } }); skip,
take,
cursor,
where,
orderBy,
}: {
skip?: number;
take?: number;
cursor?: Prisma.ClassWhereUniqueInput;
where?: Prisma.ClassWhereInput;
orderBy?: Record<string, unknown>;
}) {
return await this.prisma.class.findMany({
skip,
take,
cursor,
where,
orderBy,
});
} }
async findOne(id: string) { 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) { async update(id: string, updateClassDto: UpdateClassDto) {
@ -29,4 +48,10 @@ export class ClassService {
async remove(id: string) { async remove(id: string) {
return await this.prisma.class.delete({ where: { id } }); return await this.prisma.class.delete({ where: { id } });
} }
async bulkRemove(ids: string[]) {
return await this.prisma.class.deleteMany({
where: { id: { in: ids } },
});
}
} }

View File

@ -1,3 +1,8 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsString } from "class-validator";
export class CreateClassDto { export class CreateClassDto {
@IsString()
@ApiProperty()
name: string; name: string;
} }

View File

@ -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;

View File

@ -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[];
}

View File

@ -1,12 +1,5 @@
import { PartialType } from "@nestjs/mapped-types"; import { PartialType } from "@nestjs/mapped-types";
import { IsString } from "class-validator";
import { CreateUserDTO } from "./create-user.dto"; import { CreateUserDTO } from "./create-user.dto";
export class UpdateUserDTO extends PartialType(CreateUserDTO) { export class UpdateUserDTO extends PartialType(CreateUserDTO) {}
@IsString()
id: string;
@IsString()
username: string;
}

View File

@ -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 { import {
Body, Body,
Controller, Controller,
Delete, Delete,
Get, Get,
Post, Param,
Patch,
Query, Query,
UseGuards, UseGuards
} from "@nestjs/common"; } from "@nestjs/common";
import { import {
ApiBearerAuth, ApiBearerAuth,
ApiBody,
ApiOkResponse, ApiOkResponse,
ApiParam,
ApiQuery, ApiQuery,
ApiUnauthorizedResponse, ApiUnauthorizedResponse
} from "@nestjs/swagger"; } from "@nestjs/swagger";
import {
import { Roles } from "@/modules/auth/decorators/roles.decorator"; UserCountResponse,
import { JwtAuthGuard } from "@/modules/auth/guards/jwt.guard"; UserResponse,
import { RolesGuard } from "@/modules/auth/guards/role.guard"; UsersResponse,
} from "./ApiResponses/UserReponse";
import { BulkDeleteUserDTO } from "./dto/bulk-delete-user.dto"; import { UpdateUserDTO } from "./dto/update-user.dto";
import { CreateUserDTO } from "./dto/create-user.dto";
import { UserEntity } from "./entities/user.entity"; import { UserEntity } from "./entities/user.entity";
import { UserService } from "./user.service"; import { UserService } from "./user.service";
@Controller() @Controller("user")
@UseGuards(RolesGuard) @UseGuards(RolesGuard)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiUnauthorizedResponse({ @Roles(["ADMIN"])
description: "Unauthorized", @ApiUnauthorizedResponse(UnauthorizedResponse)
example: {
message: "Unauthorized",
statusCode: 401,
},
})
export class UserController { export class UserController {
constructor(private readonly userService: UserService) {} constructor(private readonly userService: UserService) {}
@Get("users") @Get()
@Roles(["ADMIN"]) @ApiOkResponse(UsersResponse)
@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[],
},
},
})
async findAll(): Promise<UserEntity[]> { async findAll(): Promise<UserEntity[]> {
return await this.userService return await this.userService
.findAll() .findAll()
.then((users) => users.map((user) => new UserEntity(user))); .then((users) => users.map((user) => new UserEntity(user)));
} }
@Get("user") @Get(":id")
@Roles(["ADMIN"]) @ApiOkResponse(UserResponse)
@ApiOkResponse({ async findOne(@Param("id") id: string): Promise<UserEntity> {
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<UserEntity> {
return this.userService return this.userService
.findById(id) .findOne(id)
.then((user) => new UserEntity(user)); .then((user) => new UserEntity(user));
} }
@Post("user") @Patch(":id")
@Roles(["ADMIN"]) async update(
@ApiOkResponse({ @Param("id") id: string,
type: UserEntity, @Body() updateUserDto: UpdateUserDTO,
description: "The user has been successfully created.", ): Promise<UserEntity> {
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<UserEntity> {
return this.userService return this.userService
.create(createUserInput) .update(id, updateUserDto)
.then((user) => new UserEntity(user)); .then((user) => new UserEntity(user));
} }
@Delete("user") @Delete(":id")
@Roles(["ADMIN"]) @ApiOkResponse(UserResponse)
@ApiOkResponse({ @ApiParam({
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({
name: "id", name: "id",
type: String, type: String,
description: "The user id", description: "The user id",
example: "1", example: "1",
}) })
async delete(@Query("id") id: string): Promise<UserEntity> { async remove(@Param("id") id: string): Promise<UserEntity> {
return this.userService.delete(id).then((user) => new UserEntity(user)); return this.userService.remove(id).then((user) => new UserEntity(user));
} }
@Delete("users") @Delete()
@Roles(["ADMIN"]) @ApiOkResponse(UserCountResponse)
@ApiOkResponse({ @ApiQuery({ name: "ids", required: true, type: [String] })
description: "The users have been successfully deleted.", bulkRemove(@Query("ids") ids: string | string[]): Promise<{
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<{
count: number; count: number;
}> { }> {
return this.userService.bulkDelete(ids); if (typeof ids === "string") ids = [ids];
return this.userService.bulkRemove(ids);
} }
} }

View File

@ -31,7 +31,7 @@ export class UserGateway implements OnGatewayConnection, OnGatewayDisconnect {
return client.disconnect(); return client.disconnect();
} }
const user = await this.userService.findById(jwtDecoded.id); const user = await this.userService.findOne(jwtDecoded.id);
if (!user) { if (!user) {
client.emit("auth", "User not found"); client.emit("auth", "User not found");

View File

@ -1,4 +1,4 @@
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { PrismaService } from "nestjs-prisma"; import { PrismaService } from "nestjs-prisma";
@ -10,61 +10,66 @@ export class UserService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
async findAll({ async findAll({
include, skip,
cursor,
take, take,
cursor,
where,
orderBy,
}: { }: {
include?: Prisma.UserInclude; skip?: number;
cursor?: string;
take?: number; take?: number;
cursor?: Prisma.UserWhereUniqueInput;
where?: Prisma.UserWhereInput;
orderBy?: Record<string, unknown>;
} = {}) { } = {}) {
return await this.prisma.user.findMany({ return await this.prisma.user.findMany({
include, skip,
cursor: cursor ? { id: cursor } : undefined, take,
take: take || 10, cursor,
where,
orderBy,
}); });
} }
async findById(id: string = null) { async findOne(id: string) {
return await this.prisma.user.findUnique({ return await this.prisma.user.findUniqueOrThrow({
where: { id }, where: { id },
}); });
} }
async findOne(where: Prisma.UserWhereInput) { async findByProviderId(providerId: string) {
return await this.prisma.user.findFirst({ return await this.prisma.user.findUniqueOrThrow({
where, where: {
}); providerId,
}
async create(createUserInput: CreateUserDTO) {
return await this.prisma.user.create({
data: {
username: createUserInput.username,
providerId: createUserInput.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({ return await this.prisma.user.update({
where: { id: updateUserInput.id }, where: { id },
data: { data: {
username: updateUserInput.username, username: updateUserInput.username,
}, },
}); });
} }
async delete(id: string) { async remove(id: string) {
const exist = await this.prisma.user.findUnique({ where: { id } });
if (!exist) throw new NotFoundException("User not found");
return await this.prisma.user.delete({ return await this.prisma.user.delete({
where: { id }, where: { id },
}); });
} }
async bulkDelete(ids: string[]) { async bulkRemove(ids: string[]) {
return await this.prisma.user.deleteMany({ return await this.prisma.user.deleteMany({
where: { where: {
id: { id: {