diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index cfe890d..02048d6 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,10 +1,29 @@ import {Controller, Req, Post, Options, Header, HttpCode} from '@nestjs/common'; import {ApiResponse, ApiTags, ApiBody} from '@nestjs/swagger'; -import {ALLOW_ORIGIN_ALL, ALLOW_METHOD, ALLOW_CREDENTIALS, CONTENT_LENGTH, ALLOW_HEADERS, AUTH_CONTROLLER} from '../consts'; -import {UserService} from '../users/users.service'; -import {AuthRequest, TokenResponse, RefreshAuthRequest, CheckAuthTokenRequest} from '../users/users.schema'; -import {Request} from 'express'; +import { + ALLOW_ORIGIN_ALL, + ALLOW_METHOD, + ALLOW_CREDENTIALS, + CONTENT_LENGTH, + ALLOW_HEADERS, + AUTH_CONTROLLER, +} from '../consts'; +import { UserService } from '../users/users.service'; +import { + AuthRequest, + TokenResponse, + RefreshAuthRequest, + CheckAuthTokenRequest, +} from '../users/users.schema'; +import { Request } from 'express'; +import { + AUTH_SUCCESS, + AUTH_BAD_REQUEST, + REFRESH_AUTH_SUCCESS, + REFRESH_AUTH_BAD_REQUEST, + CHECK_AUTH_TOKEN, +} from './auth.responses'; @Controller(AUTH_CONTROLLER) @@ -16,14 +35,11 @@ export class AuthController { @Post() @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 201, - description: 'Метод авторизации пользователя', - type: TokenResponse, - }) + @ApiResponse(AUTH_SUCCESS) + @ApiResponse(AUTH_BAD_REQUEST) @ApiBody({ type: AuthRequest, - description: 'Ожидает логин и пароль пользователя для авторизации' + description: 'Объект с логином и паролем пользователя для авторизации' }) async authUser(@Req() request: Request): Promise { const host = request.headers.host; @@ -33,11 +49,8 @@ export class AuthController { @Post('refresh') @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 201, - description: 'Метод обновления токенов', - type: TokenResponse, - }) + @ApiResponse(REFRESH_AUTH_SUCCESS) + @ApiResponse(REFRESH_AUTH_BAD_REQUEST) @ApiBody({ type: RefreshAuthRequest, description: 'Токен для сброса токенов' @@ -50,18 +63,13 @@ export class AuthController { } @Post('check') - @ApiResponse({ - status: 200, - description: 'Проверяет токен авторизации', - type: Boolean, - }) + @ApiResponse(CHECK_AUTH_TOKEN) @ApiBody({ type: CheckAuthTokenRequest, description: 'Токен для проверки', }) async checkAccessToken(@Req() request: Request): Promise { const {access_token, host, agent} = request.body; - console.log(request.headers) return this.userService.checkAccessToken(access_token, host, agent); } diff --git a/src/auth/auth.responses.ts b/src/auth/auth.responses.ts new file mode 100644 index 0000000..077738b --- /dev/null +++ b/src/auth/auth.responses.ts @@ -0,0 +1,43 @@ +import {ApiResponseOptions, ApiProperty} from '@nestjs/swagger'; +import {TokenResponse} from 'src/users/users.schema'; + +class Error { + @ApiProperty() + statusCode: number; + + @ApiProperty() + message: string; + + @ApiProperty() + error: string; +} + +export const AUTH_SUCCESS: ApiResponseOptions = { + status: 201, + description: 'Объект с парой токенов авторизации', + type: TokenResponse, +}; + +export const AUTH_BAD_REQUEST: ApiResponseOptions = { + status: 400, + description: 'Ошибка при неверном логине или пароле', + type: Error, +}; + +export const REFRESH_AUTH_SUCCESS: ApiResponseOptions = { + status: 201, + description: 'Метод обновления токенов', + type: TokenResponse, +}; + +export const REFRESH_AUTH_BAD_REQUEST: ApiResponseOptions = { + status: 400, + description: 'Ошибка при попытке прислать просроченный или не валидный токен', + type: Error, +}; + +export const CHECK_AUTH_TOKEN: ApiResponseOptions = { + status: 200, + description: 'Проверяет токен авторизации', + type: Boolean, +}; diff --git a/src/users/users.contoller.ts b/src/users/users.contoller.ts index 673eab3..228ad7b 100644 --- a/src/users/users.contoller.ts +++ b/src/users/users.contoller.ts @@ -1,10 +1,34 @@ import {Controller, Get, Req, Post, Options, Header, Delete, HttpCode, Put} from '@nestjs/common'; -import {ApiResponse, ApiTags, ApiParam, ApiBody} from '@nestjs/swagger'; -import {ALLOW_ORIGIN_ALL, ALLOW_METHOD, ALLOW_CREDENTIALS, CONTENT_LENGTH, ALLOW_HEADERS, USERS_CONTROLLER} from '../consts'; -import {UserService} from './users.service'; -import {UserResponse, CreateUserRequest, UpdateUserRequest} from './users.schema'; -import {Request} from 'express'; +import { ApiResponse, ApiTags, ApiParam, ApiBody } from '@nestjs/swagger'; +import { + ALLOW_ORIGIN_ALL, + ALLOW_METHOD, + ALLOW_CREDENTIALS, + CONTENT_LENGTH, + ALLOW_HEADERS, + USERS_CONTROLLER, +} from '../consts'; +import { UserService } from './users.service'; +import { + UserResponse, + CreateUserRequest, + UpdateUserRequest, +} from './users.schema'; +import { Request } from 'express'; +import { + FIND_ALL_SUCCESS, + FIND_ONE_SUCCESS, + FIND_ONE_NOT_FOUND, + CREATE_SUCCESS, + CREATE_CONFLICT, + CREATE_NOT_VALID, + UPDATE_SUCCESS, + UPDATE_NOT_FOUND, + UPDATE_NOT_VALID, + REMOVE_SUCCESS, + REMOVE_NOT_FOUND, +} from './users.responses'; @Controller(USERS_CONTROLLER) @ApiTags(USERS_CONTROLLER) @@ -15,21 +39,15 @@ export class UsersController { @Get() @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 200, - description: 'Список всех пользователей', - type: [UserResponse] - }) + @ApiResponse(FIND_ALL_SUCCESS) async findAll(): Promise { return this.userService.findAll(); } @Get(':login') @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 200, - description: 'Получить пользователя по логину' - }) + @ApiResponse(FIND_ONE_SUCCESS) + @ApiResponse(FIND_ONE_NOT_FOUND) @ApiParam({ name: 'login', description: 'Логин пользователя', @@ -40,14 +58,12 @@ export class UsersController { @Post() @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 201, - description: 'Создает пользователя в системе', - type: UserResponse, - }) + @ApiResponse(CREATE_SUCCESS) + @ApiResponse(CREATE_CONFLICT) + @ApiResponse(CREATE_NOT_VALID) @ApiBody({ type: CreateUserRequest, - description: 'Принимает объект для создания пользователя' + description: 'Объект для создания пользователя' }) async createUser(@Req() request: Request): Promise { return await this.userService.create(request.body); @@ -55,14 +71,12 @@ export class UsersController { @Put() @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 200, - description: 'Обновляет данные пользователя', - type: UpdateUserRequest - }) + @ApiResponse(UPDATE_SUCCESS) + @ApiResponse(UPDATE_NOT_FOUND) + @ApiResponse(UPDATE_NOT_VALID) @ApiBody({ type: UpdateUserRequest, - description: 'Принимает объект для обновления данных пользователя' + description: 'Объект обновления данных пользователя' }) async updateUser(@Req() request: Request): Promise { return await this.userService.update(request.body); @@ -70,11 +84,8 @@ export class UsersController { @Delete(':login') @Header(...ALLOW_ORIGIN_ALL) - @ApiResponse({ - status: 200, - description: 'Удаляет пользователя', - type: UpdateUserRequest, - }) + @ApiResponse(REMOVE_SUCCESS) + @ApiResponse(REMOVE_NOT_FOUND) @ApiParam({ name: 'login', description: 'Логин пользователя', diff --git a/src/users/users.responses.ts b/src/users/users.responses.ts new file mode 100644 index 0000000..cdc2012 --- /dev/null +++ b/src/users/users.responses.ts @@ -0,0 +1,80 @@ +import {ApiResponseOptions, ApiProperty} from '@nestjs/swagger'; +import {UserResponse, UpdateUserRequest} from './users.schema'; + +class Error { + @ApiProperty() + statusCode: number; + + @ApiProperty() + message: string; + + @ApiProperty() + error: string; +} + +export const FIND_ALL_SUCCESS: ApiResponseOptions = { + status: 200, + description: 'Список всех пользователей', + type: UserResponse, + isArray: true +}; + +export const FIND_ONE_SUCCESS: ApiResponseOptions = { + status: 200, + description: 'Пользователь найденный по логину', + type: UserResponse, +}; + +export const FIND_ONE_NOT_FOUND: ApiResponseOptions = { + status: 404, + description: 'Ошибка при попытке получить несуществующего пользователя', + type: Error, +}; + +export const CREATE_SUCCESS: ApiResponseOptions = { + status: 201, + description: 'Создает пользователя в системе', + type: UserResponse, +}; + +export const CREATE_CONFLICT: ApiResponseOptions = { + status: 409, + description: 'Объект вновь созданного пользователя', + type: Error, +}; + +export const CREATE_NOT_VALID: ApiResponseOptions = { + status: 400, + description: 'Ошибка при попытке создания пользователя с уже существующим логином', + type: Error, +}; + +export const UPDATE_SUCCESS: ApiResponseOptions = { + status: 200, + description: 'Объект для обновления пользователя', + type: UpdateUserRequest, +}; + +export const UPDATE_NOT_FOUND: ApiResponseOptions = { + status: 404, + description: 'Ошибка при попытке обновить пользователя с несуществующим логином', + type: Error, +}; + +export const UPDATE_NOT_VALID: ApiResponseOptions = { + status: 400, + description: 'Ошибка при попытке обновить пользователя с невалидными полями', + type: Error, +}; + +export const REMOVE_SUCCESS: ApiResponseOptions = { + status: 200, + description: 'Объект удаленного пользователя', + type: UpdateUserRequest, +}; + +export const REMOVE_NOT_FOUND: ApiResponseOptions = { + status: 404, + description: 'Ошибка при попытке удалить пользователя с несуществующим логином', + type: Error, +}; diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 5d5f52a..2ec33bb 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,5 +1,5 @@ import {Model, Connection, Document} from 'mongoose'; -import {Injectable, NotFoundException, BadGatewayException, UnauthorizedException} from '@nestjs/common'; +import {Injectable, NotFoundException, BadGatewayException, ConflictException, BadRequestException} from '@nestjs/common'; import {InjectConnection} from '@nestjs/mongoose'; import {DB_NAME, USERS_CONTROLLER, SECRET_JWT_ACCESS_KEY, SECRET_JWT_REFRESH_KEY} from 'src/consts'; import {User, UserSchema, CreateUserRequest, UserResponse, UserModel, UpdateUserRequest, TokenResponse} from './users.schema'; @@ -53,7 +53,7 @@ export class UserService { async findOne(login: string): Promise { const user = await this.userModel().findOne({login}); if (!user) { - throw new NotFoundException(`Пользователь с логином ${login} не найден`); + throw new NotFoundException(`Not found user "${login}"`); } return prepareUserToUserResponse(user); } @@ -67,19 +67,29 @@ export class UserService { const searchUser = await this.findUser(user.login); if (searchUser) { - throw new NotFoundException(`Пользователь с логином ${user.login} уже существует`); + throw new ConflictException(`User login "${user.login}" is already in use`); + } + + const Model = await this.userModel(); + + try { + const checkUser = new Model(user); + await validateModel(checkUser); + } catch (e) { + if (e?.message?.includes('validation failed')) { + throw new BadRequestException(e.message); + } + throw e; } const salt = await bcrypt.genSalt(10); const password = await bcrypt.hash(user.password, salt); - const Model = await this.userModel(); const createUser = new Model({ ...user, salt, password, }); - await validateModel(createUser); const savedUser = await createUser.save(); @@ -89,12 +99,24 @@ export class UserService { async update(user: UpdateUserRequest): Promise { const searchUser = await this.userModel().findOne({login: user.login}); + if (!searchUser) { + throw new NotFoundException(`Not found user login "${user.login}"`) + } + const Model = await this.userModel(); const updateUser = new Model({ ...user, password: searchUser.password, }); - await validateModel(updateUser); + + try { + await validateModel(updateUser); + } catch (e) { + if (e?.message?.includes('validation failed')) { + throw new BadRequestException(e.message); + } + throw e; + } await searchUser.updateOne({ ...{ @@ -108,6 +130,10 @@ export class UserService { async removeOne(login: string): Promise { const searchUser = await this.userModel().findOne({login}); + if (!searchUser) { + throw new NotFoundException(`Not found user login "${login}"`); + } + await this.userModel().deleteOne({login}); return prepareUserToUserResponse(searchUser); @@ -144,14 +170,14 @@ export class UserService { return this.generateTokens(login, host, agent); } - throw new UnauthorizedException('Не верный пользователь или пароль'); + throw new BadRequestException('Invalid user or password'); } verifyToken(token: string, secret: string): void { try { jwt.verify(token, secret); } catch (e) { - throw new UnauthorizedException('Авторизация устарела'); + throw new BadRequestException('Authorization is outdated'); } } @@ -166,11 +192,16 @@ export class UserService { if (searchUser && this.checkToken(token, host, agent)) { return this.generateTokens(token.login, host, agent); } - throw new UnauthorizedException('Не санкционированный запрос'); + throw new BadRequestException('Unauthorized request'); } async checkAccessToken(access_token: string, host: string, agent: string): Promise { - this.verifyToken(access_token, SECRET_JWT_ACCESS_KEY); + try { + this.verifyToken(access_token, SECRET_JWT_ACCESS_KEY); + } catch (e) { + return false; + } + const token = jwt.decode(access_token) as Token; const searchUser = await this.findUser(token.login); return searchUser && this.checkToken(token, host, agent); diff --git a/users.http b/users.http index 8c9cb14..6fb850d 100644 --- a/users.http +++ b/users.http @@ -40,7 +40,7 @@ POST http://localhost:4002/auth/refresh HTTP/1.1 content-type: application/json { - "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImhvc3QiOiJsb2NhbGhvc3Q6NDAwMiIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTU3OTA0MjgsImV4cCI6MTU5NTg3NjgyOH0.4gsUq6rGT917BBqNT6PMAONvGVzscE0WqeU6pyCpAw0" + "refresh_token": "eyJhbGciOiJIUz3I1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImhvc3QiOiJsb2NhbGhvc3Q6NDAwMiIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTYyMzE3NjIsImV4cCI6MTU5NjMxODE2Mn0.nRU429auiyhqrmIr5qXc8kjnWZXtmWYE50iDKOPFmkk" } ### Проверить токен @@ -48,7 +48,5 @@ POST http://localhost:4002/auth/check HTTP/1.1 content-type: application/json { - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImhvc3QiOiJsb2NhbGhvc3Q6NDAwMiIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTU3OTA0NDYsImV4cCI6MTU5NTc5MDU2Nn0.OMMDMgDp45RGBiBGRPf7hZ9_gNt7rKD1Ypwyt5cHVfc", - "host": "localhost:4002", - "agent": "vscode-restclient" + "access_token": "eyаJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbiI6InN0cmluZyIsImhvc3QiOiJsb2NhbGhvc3Q6NDAwMiIsImFnZW50IjoidnNjb2RlLXJlc3RjbGllbnQiLCJpYXQiOjE1OTYyMzE1MzQsImV4cCI6MTU5NjIzMTY1NH0.muSl2TE2gQ78UxfaufO5SWszN5h0yYbPvR5_1PB-d2c" }