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

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

У 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

View File

@@ -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<UpdateTokenResDto> {
@@ -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,

View File

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

View File

@@ -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"]) {}

View File

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

View File

@@ -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<ClientUserDto> {
async getMe(@UserToken() token: string): Promise<ClientUserResDto> {
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 { 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],
})

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