diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..29f8f8c --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-js:latest diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index dd3a7b7..e14ea0c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -51,7 +51,7 @@ export class AuthController { }) @ResultDto(SignInResDto) @HttpCode(HttpStatus.OK) - @Post("signIn") + @Post("sign-in") signIn(@Body() signInDto: SignInReqDto) { return this.authService.signIn(signInDto); } @@ -69,7 +69,7 @@ export class AuthController { }) @ResultDto(SignUpResDto) @HttpCode(HttpStatus.CREATED) - @Post("signUp") + @Post("sign-up") async signUp(@Body() signUpDto: SignUpReqDto) { if ( !(await this.scheduleService.getGroupNames()).names.includes( @@ -88,7 +88,7 @@ export class AuthController { @ApiExtraModels(UpdateTokenResDto) @ApiOperation({ summary: "Обновление просроченного токена", - tags: ["auth", "accessToken"], + tags: ["auth", "access-token"], }) @ApiBody({ schema: refs(UpdateTokenReqDto)[0] }) @ApiOkResponse({ @@ -100,7 +100,7 @@ export class AuthController { }) @ResultDto(UpdateTokenResDto) @HttpCode(HttpStatus.OK) - @Post("updateToken") + @Post("update-token") updateToken( @Body() updateTokenDto: UpdateTokenReqDto, ): Promise { @@ -121,7 +121,7 @@ export class AuthController { }) @ResultDto(null) @HttpCode(HttpStatus.OK) - @Post("changePassword") + @Post("change-password") async changePassword( @Body() changePasswordReqDto: ChangePasswordReqDto, @UserToken() userToken: string, diff --git a/src/dto/auth.dto.ts b/src/dto/auth.dto.ts index 7a8379d..7d391cc 100644 --- a/src/dto/auth.dto.ts +++ b/src/dto/auth.dto.ts @@ -5,7 +5,10 @@ import { Expose } from "class-transformer"; // SignIn export class SignInReqDto extends PickType(UserDto, ["username"]) { - @ApiProperty({ description: "Пароль в исходном виде" }) + @ApiProperty({ + example: "my-password", + description: "Пароль в исходном виде", + }) @IsString() password: string; } diff --git a/src/dto/user.dto.ts b/src/dto/user.dto.ts index b19bc20..dffac52 100644 --- a/src/dto/user.dto.ts +++ b/src/dto/user.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, OmitType } from "@nestjs/swagger"; +import { ApiProperty, OmitType, PickType } from "@nestjs/swagger"; import { IsEnum, IsJWT, @@ -16,7 +16,10 @@ export enum UserRoleDto { } export class UserDto { - @ApiProperty({ description: "Идентификатор (ObjectId)" }) + @ApiProperty({ + example: "66e1b7e255c5d5f1268cce90", + description: "Идентификатор (ObjectId)", + }) @IsMongoId() @Expose() id: string; @@ -66,14 +69,20 @@ export class UserDto { role: UserRoleDto; } -export class ClientUserDto extends OmitType(UserDto, [ +export class ClientUserResDto extends OmitType(UserDto, [ "password", "salt", "accessToken", ]) { - static fromUserDto(userDto: UserDto): ClientUserDto { - return plainToClass(ClientUserDto, userDto, { + static fromUserDto(userDto: UserDto): ClientUserResDto { + return plainToClass(ClientUserResDto, userDto, { excludeExtraneousValues: true, }); } } + +// changes + +export class ChangeUsernameReqDto extends PickType(UserDto, ["username"]) {} + +export class ChangeGroupReqDto extends PickType(UserDto, ["group"]) {} diff --git a/src/schedule/schedule.controller.ts b/src/schedule/schedule.controller.ts index add0e85..ad8146c 100644 --- a/src/schedule/schedule.controller.ts +++ b/src/schedule/schedule.controller.ts @@ -54,7 +54,7 @@ export class ScheduleController { @ApiNotFoundResponse({ description: "Требуемая группа не найдена" }) @ResultDto(GroupScheduleDto) @HttpCode(HttpStatus.OK) - @Post("getGroup") + @Post("get-group") getGroupSchedule( @Body() groupDto: GroupScheduleRequestDto, ): Promise { @@ -73,7 +73,7 @@ export class ScheduleController { @ApiNotFoundResponse({ description: "Требуемая группа не найдена" }) @ResultDto(ScheduleGroupsDto) @HttpCode(HttpStatus.OK) - @Get("getGroupNames") + @Get("get-group-names") async getGroupNames(): Promise { return this.scheduleService.getGroupNames(); } diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index 7967b1c..0993ee0 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,27 +1,93 @@ import { + Body, Controller, Get, HttpCode, HttpStatus, + Post, UseGuards, } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; -import { ClientUserDto } from "../dto/user.dto"; +import { + ChangeGroupReqDto, + ChangeUsernameReqDto, + ClientUserResDto, +} from "../dto/user.dto"; import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { UserToken } from "../auth/auth.decorator"; import { AuthService } from "../auth/auth.service"; +import { UsersService } from "./users.service"; +import { + ApiBody, + ApiConflictResponse, + ApiExtraModels, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + refs, +} from "@nestjs/swagger"; @Controller("api/v1/users") @UseGuards(AuthGuard) export class UsersController { - constructor(private readonly authService: AuthService) {} + constructor( + private readonly authService: AuthService, + private readonly usersService: UsersService, + ) {} - @ResultDto(ClientUserDto) + @ApiExtraModels(ClientUserResDto) + @ApiOperation({ + summary: "Получение данных о профиле пользователя", + tags: ["users"], + }) + @ApiOkResponse({ + description: "Получение профиля прошло успешно", + schema: refs(ClientUserResDto)[0], + }) + @ResultDto(ClientUserResDto) @HttpCode(HttpStatus.OK) @Get("me") - async getMe(@UserToken() token: string): Promise { + async getMe(@UserToken() token: string): Promise { const userDto = await this.authService.decodeUserToken(token); - return ClientUserDto.fromUserDto(userDto); + return ClientUserResDto.fromUserDto(userDto); + } + + @ApiExtraModels(ChangeUsernameReqDto) + @ApiOperation({ summary: "Смена имени пользователя", tags: ["users"] }) + @ApiBody({ schema: refs(ChangeUsernameReqDto)[0] }) + @ApiOkResponse({ description: "Смена имени профиля прошла успешно" }) + @ApiConflictResponse({ + description: "Пользователь с таким именем уже существует", + }) + @ResultDto(null) + @HttpCode(HttpStatus.OK) + @Post("change-username") + async changeUsername( + @Body() changeUsernameDto: ChangeUsernameReqDto, + @UserToken() token: string, + ): Promise { + const user = await this.authService.decodeUserToken(token); + + return await this.usersService.changeUsername(user, changeUsernameDto); + } + + @ApiExtraModels(ChangeGroupReqDto) + @ApiOperation({ summary: "Смена группы пользователя", tags: ["users"] }) + @ApiBody({ schema: refs(ChangeGroupReqDto)[0] }) + @ApiOkResponse({ description: "Смена группы прошла успешно" }) + @ApiNotFoundResponse({ + description: "Группа с таким названием не существует", + }) + @ResultDto(null) + @HttpCode(HttpStatus.OK) + @Post("change-group") + async changeGroup( + @Body() changeGroupDto: ChangeGroupReqDto, + @UserToken() token: string, + ): Promise { + const user = await this.authService.decodeUserToken(token); + + return await this.usersService.changeGroup(user, changeGroupDto); } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 11c8a1f..6f06a92 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -3,9 +3,10 @@ import { UsersService } from "./users.service"; import { PrismaService } from "../prisma/prisma.service"; import { UsersController } from "./users.controller"; import { AuthService } from "../auth/auth.service"; +import { ScheduleService } from "../schedule/schedule.service"; @Module({ - providers: [PrismaService, UsersService, AuthService], + providers: [PrismaService, UsersService, AuthService, ScheduleService], exports: [UsersService], controllers: [UsersController], }) diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 2ccb4a7..b7fcf33 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,11 +1,23 @@ -import { Injectable } from "@nestjs/common"; +import { + ConflictException, + Injectable, + NotFoundException, +} from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { Prisma } from "@prisma/client"; -import { UserDto } from "../dto/user.dto"; +import { + ChangeGroupReqDto, + ChangeUsernameReqDto, + UserDto, +} from "../dto/user.dto"; +import { ScheduleService } from "../schedule/schedule.service"; @Injectable() export class UsersService { - constructor(private readonly prismaService: PrismaService) {} + constructor( + private readonly prismaService: PrismaService, + private readonly scheduleService: ScheduleService, + ) {} private static convertToDto = (user: UserDto | null) => user as UserDto | null; @@ -38,4 +50,41 @@ export class UsersService { .count({ where }) .then((count) => count > 0); } + + async changeUsername( + user: UserDto, + changeUsernameDto: ChangeUsernameReqDto, + ): Promise { + if (user.username === changeUsernameDto.username) return; + + if (await this.contains({ username: changeUsernameDto.username })) { + throw new ConflictException( + "Пользователь с таким именем уже существует", + ); + } + + await this.update({ + where: { id: user.id }, + data: { username: changeUsernameDto.username }, + }); + } + + async changeGroup( + user: UserDto, + changeGroupDto: ChangeGroupReqDto, + ): Promise { + if (user.group === changeGroupDto.group) return; + + const groupNames = await this.scheduleService.getGroupNames(); + if (!groupNames.names.includes(changeGroupDto.group)) { + throw new NotFoundException( + "Группа с таким названием не существует", + ); + } + + await this.update({ + where: { id: user.id }, + data: { group: changeGroupDto.group }, + }); + } } diff --git a/src/utility/validation/is-map.ts b/src/utility/validation/is-map.ts deleted file mode 100644 index 202c824..0000000 --- a/src/utility/validation/is-map.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { - isObject, - registerDecorator, - ValidationArguments, - ValidationOptions, -} from "class-validator"; - -export function IsMap( - keyValidators: ((value: unknown) => boolean)[], - valueValidators: ((value: unknown) => boolean)[], - validationOptions?: ValidationOptions, -) { - return function (object: unknown, propertyName: string) { - registerDecorator({ - name: "isMap", - target: (object as any).constructor, - propertyName: propertyName, - options: validationOptions, - validator: { - validate(value: unknown, args: ValidationArguments): boolean { - if (!isObject(value)) return false; - const keys = Object.keys(value); - const isInvalid = keys.some((key) => { - const isKeyInvalid = keyValidators.some( - (validator) => !validator(key), - ); - if (isKeyInvalid) return true; - - return valueValidators.some( - (validator) => !validator(value[key]), - ); - }); - - return !isInvalid; - }, - }, - }); - }; -}