Первый релиз.

Названия конечных точек теперь пишутся в нижнем регистре через знак минуса.

У DTO добавлена пара недостающих примеров в документации.

Удалён неиспользуемый декоратор IsMap.

users.controller.ts
- Описана конечная точка "me".
- Добавлены конечные точки "change-username" и "change-group", для смены имени пользователя и группы соответственно.

users.service.ts
- Добавлены методы "changeUsername" и "changeGroup", для смены имени пользователя и группы соответственно.
This commit is contained in:
2024-09-15 15:40:13 +04:00
parent 9c7a87fc65
commit a6d88a457f
9 changed files with 179 additions and 61 deletions

29
qodana.yaml Normal file
View File

@@ -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: <SomeEnabledInspectionId>
#Disable inspections
#exclude:
# - name: <SomeDisabledInspectionId>
# paths:
# - <path/where/not/run/inspection>
#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> #(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

View File

@@ -51,7 +51,7 @@ export class AuthController {
}) })
@ResultDto(SignInResDto) @ResultDto(SignInResDto)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("signIn") @Post("sign-in")
signIn(@Body() signInDto: SignInReqDto) { signIn(@Body() signInDto: SignInReqDto) {
return this.authService.signIn(signInDto); return this.authService.signIn(signInDto);
} }
@@ -69,7 +69,7 @@ export class AuthController {
}) })
@ResultDto(SignUpResDto) @ResultDto(SignUpResDto)
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@Post("signUp") @Post("sign-up")
async signUp(@Body() signUpDto: SignUpReqDto) { async signUp(@Body() signUpDto: SignUpReqDto) {
if ( if (
!(await this.scheduleService.getGroupNames()).names.includes( !(await this.scheduleService.getGroupNames()).names.includes(
@@ -88,7 +88,7 @@ export class AuthController {
@ApiExtraModels(UpdateTokenResDto) @ApiExtraModels(UpdateTokenResDto)
@ApiOperation({ @ApiOperation({
summary: "Обновление просроченного токена", summary: "Обновление просроченного токена",
tags: ["auth", "accessToken"], tags: ["auth", "access-token"],
}) })
@ApiBody({ schema: refs(UpdateTokenReqDto)[0] }) @ApiBody({ schema: refs(UpdateTokenReqDto)[0] })
@ApiOkResponse({ @ApiOkResponse({
@@ -100,7 +100,7 @@ export class AuthController {
}) })
@ResultDto(UpdateTokenResDto) @ResultDto(UpdateTokenResDto)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("updateToken") @Post("update-token")
updateToken( updateToken(
@Body() updateTokenDto: UpdateTokenReqDto, @Body() updateTokenDto: UpdateTokenReqDto,
): Promise<UpdateTokenResDto> { ): Promise<UpdateTokenResDto> {
@@ -121,7 +121,7 @@ export class AuthController {
}) })
@ResultDto(null) @ResultDto(null)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("changePassword") @Post("change-password")
async changePassword( async changePassword(
@Body() changePasswordReqDto: ChangePasswordReqDto, @Body() changePasswordReqDto: ChangePasswordReqDto,
@UserToken() userToken: string, @UserToken() userToken: string,

View File

@@ -5,7 +5,10 @@ import { Expose } from "class-transformer";
// SignIn // SignIn
export class SignInReqDto extends PickType(UserDto, ["username"]) { export class SignInReqDto extends PickType(UserDto, ["username"]) {
@ApiProperty({ description: "Пароль в исходном виде" }) @ApiProperty({
example: "my-password",
description: "Пароль в исходном виде",
})
@IsString() @IsString()
password: string; password: string;
} }

View File

@@ -1,4 +1,4 @@
import { ApiProperty, OmitType } from "@nestjs/swagger"; import { ApiProperty, OmitType, PickType } from "@nestjs/swagger";
import { import {
IsEnum, IsEnum,
IsJWT, IsJWT,
@@ -16,7 +16,10 @@ export enum UserRoleDto {
} }
export class UserDto { export class UserDto {
@ApiProperty({ description: "Идентификатор (ObjectId)" }) @ApiProperty({
example: "66e1b7e255c5d5f1268cce90",
description: "Идентификатор (ObjectId)",
})
@IsMongoId() @IsMongoId()
@Expose() @Expose()
id: string; id: string;
@@ -66,14 +69,20 @@ export class UserDto {
role: UserRoleDto; role: UserRoleDto;
} }
export class ClientUserDto extends OmitType(UserDto, [ export class ClientUserResDto extends OmitType(UserDto, [
"password", "password",
"salt", "salt",
"accessToken", "accessToken",
]) { ]) {
static fromUserDto(userDto: UserDto): ClientUserDto { static fromUserDto(userDto: UserDto): ClientUserResDto {
return plainToClass(ClientUserDto, userDto, { return plainToClass(ClientUserResDto, userDto, {
excludeExtraneousValues: true, excludeExtraneousValues: true,
}); });
} }
} }
// changes
export class ChangeUsernameReqDto extends PickType(UserDto, ["username"]) {}
export class ChangeGroupReqDto extends PickType(UserDto, ["group"]) {}

View File

@@ -54,7 +54,7 @@ export class ScheduleController {
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" }) @ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
@ResultDto(GroupScheduleDto) @ResultDto(GroupScheduleDto)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("getGroup") @Post("get-group")
getGroupSchedule( getGroupSchedule(
@Body() groupDto: GroupScheduleRequestDto, @Body() groupDto: GroupScheduleRequestDto,
): Promise<GroupScheduleDto> { ): Promise<GroupScheduleDto> {
@@ -73,7 +73,7 @@ export class ScheduleController {
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" }) @ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
@ResultDto(ScheduleGroupsDto) @ResultDto(ScheduleGroupsDto)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Get("getGroupNames") @Get("get-group-names")
async getGroupNames(): Promise<ScheduleGroupsDto> { async getGroupNames(): Promise<ScheduleGroupsDto> {
return this.scheduleService.getGroupNames(); return this.scheduleService.getGroupNames();
} }

View File

@@ -1,27 +1,93 @@
import { import {
Body,
Controller, Controller,
Get, Get,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
Post,
UseGuards, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import { AuthGuard } from "../auth/auth.guard"; 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 { ResultDto } from "../utility/validation/class-validator.interceptor";
import { UserToken } from "../auth/auth.decorator"; import { UserToken } from "../auth/auth.decorator";
import { AuthService } from "../auth/auth.service"; 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") @Controller("api/v1/users")
@UseGuards(AuthGuard) @UseGuards(AuthGuard)
export class UsersController { 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) @HttpCode(HttpStatus.OK)
@Get("me") @Get("me")
async getMe(@UserToken() token: string): Promise<ClientUserDto> { async getMe(@UserToken() token: string): Promise<ClientUserResDto> {
const userDto = await this.authService.decodeUserToken(token); 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<void> {
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<void> {
const user = await this.authService.decodeUserToken(token);
return await this.usersService.changeGroup(user, changeGroupDto);
} }
} }

View File

@@ -3,9 +3,10 @@ import { UsersService } from "./users.service";
import { PrismaService } from "../prisma/prisma.service"; import { PrismaService } from "../prisma/prisma.service";
import { UsersController } from "./users.controller"; import { UsersController } from "./users.controller";
import { AuthService } from "../auth/auth.service"; import { AuthService } from "../auth/auth.service";
import { ScheduleService } from "../schedule/schedule.service";
@Module({ @Module({
providers: [PrismaService, UsersService, AuthService], providers: [PrismaService, UsersService, AuthService, ScheduleService],
exports: [UsersService], exports: [UsersService],
controllers: [UsersController], controllers: [UsersController],
}) })

View File

@@ -1,11 +1,23 @@
import { Injectable } from "@nestjs/common"; import {
ConflictException,
Injectable,
NotFoundException,
} from "@nestjs/common";
import { PrismaService } from "../prisma/prisma.service"; import { PrismaService } from "../prisma/prisma.service";
import { Prisma } from "@prisma/client"; 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() @Injectable()
export class UsersService { export class UsersService {
constructor(private readonly prismaService: PrismaService) {} constructor(
private readonly prismaService: PrismaService,
private readonly scheduleService: ScheduleService,
) {}
private static convertToDto = (user: UserDto | null) => private static convertToDto = (user: UserDto | null) =>
user as UserDto | null; user as UserDto | null;
@@ -38,4 +50,41 @@ export class UsersService {
.count({ where }) .count({ where })
.then((count) => count > 0); .then((count) => count > 0);
} }
async changeUsername(
user: UserDto,
changeUsernameDto: ChangeUsernameReqDto,
): Promise<void> {
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<void> {
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 },
});
}
} }

View File

@@ -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;
},
},
});
};
}