From a9b1eb500d152bb37417fb9676f3256b6d25dfb4 Mon Sep 17 00:00:00 2001 From: AdonaiJehosua Date: Sun, 17 Nov 2024 18:23:50 +0500 Subject: [PATCH 1/3] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D1=81=D1=82?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8C=20Auth=20=D0=B8=20middl?= =?UTF-8?q?eware=20=D0=B4=D0=BB=D1=8F=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8?= =?UTF-8?q?=D0=BD=D0=B3=D0=B0=20=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom.d.ts | 6 +++ package-lock.json | 14 +++++ package.json | 1 + src/main.rest.ts | 2 + src/rest/rest.application.ts | 6 ++- src/shared/libs/config/rest.schema.ts | 7 +++ .../rest/middleware/parse-token.middleware.ts | 46 ++++++++++++++++ .../modules/auth/auth-service.interface.ts | 5 ++ src/shared/modules/auth/auth.constant.ts | 2 + src/shared/modules/auth/auth.container.ts | 12 +++++ .../modules/auth/auth.exception-filter.ts | 25 +++++++++ .../modules/auth/default-auth.service.ts | 52 +++++++++++++++++++ .../auth/errors/base-user.exception.ts | 6 +++ src/shared/modules/auth/errors/index.ts | 3 ++ .../auth/errors/user-not-found.exeption.ts | 7 +++ .../user-password-incorrect.exeption.ts | 7 +++ src/shared/modules/auth/index.ts | 4 ++ src/shared/modules/auth/types/TokenPayload.ts | 7 +++ .../modules/comment/comment.controller.ts | 4 +- .../modules/comment/dto/create-comment.dto.ts | 1 - src/shared/modules/user/index.ts | 3 +- .../modules/user/rdo/logged-user.rdo.ts | 7 +++ src/shared/modules/user/user.controller.ts | 27 ++++------ src/shared/modules/user/user.entity.ts | 9 ++-- src/shared/types/component.enum.ts | 2 + src/shared/types/user.type.ts | 1 - tsconfig.json | 3 +- 27 files changed, 242 insertions(+), 27 deletions(-) create mode 100644 custom.d.ts create mode 100644 src/shared/libs/rest/middleware/parse-token.middleware.ts create mode 100644 src/shared/modules/auth/auth-service.interface.ts create mode 100644 src/shared/modules/auth/auth.constant.ts create mode 100644 src/shared/modules/auth/auth.container.ts create mode 100644 src/shared/modules/auth/auth.exception-filter.ts create mode 100644 src/shared/modules/auth/default-auth.service.ts create mode 100644 src/shared/modules/auth/errors/base-user.exception.ts create mode 100644 src/shared/modules/auth/errors/index.ts create mode 100644 src/shared/modules/auth/errors/user-not-found.exeption.ts create mode 100644 src/shared/modules/auth/errors/user-password-incorrect.exeption.ts create mode 100644 src/shared/modules/auth/index.ts create mode 100644 src/shared/modules/auth/types/TokenPayload.ts create mode 100644 src/shared/modules/user/rdo/logged-user.rdo.ts diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 0000000..ded0fcd --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,6 @@ +import { TokenPayload } from './src/shared/modules/auth/index.js'; +declare module 'express-serve-static-core' { + export interface Request { + tokenPayload: TokenPayload; + } +} diff --git a/package-lock.json b/package-lock.json index ed0163f..a44a69c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "got": "13.0.0", "http-status-codes": "2.3.0", "inversify": "6.0.1", + "jose": "4.15.4", "mime-types": "2.1.35", "mongoose": "7.5.3", "multer": "1.4.5-lts.1", @@ -4741,6 +4742,14 @@ "dev": true, "license": "MIT" }, + "node_modules/jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -11181,6 +11190,11 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, + "jose": { + "version": "4.15.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", + "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + }, "joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", diff --git a/package.json b/package.json index e65fd88..e782f38 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "got": "13.0.0", "http-status-codes": "2.3.0", "inversify": "6.0.1", + "jose": "4.15.4", "mime-types": "2.1.35", "mongoose": "7.5.3", "multer": "1.4.5-lts.1", diff --git a/src/main.rest.ts b/src/main.rest.ts index 98cb124..549a1af 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -6,6 +6,7 @@ import {createRestApplicationContainer} from './rest/rest.container.js'; import {createUserContainer} from './shared/modules/user/index.js'; import {createOfferContainer} from './shared/modules/offer/index.js'; import {createCommentContainer} from './shared/modules/comment/index.js'; +import {createAuthContainer} from './shared/modules/auth/index.js'; async function bootstrap() { const appContainer = Container.merge( @@ -13,6 +14,7 @@ async function bootstrap() { createOfferContainer(), createUserContainer(), createCommentContainer(), + createAuthContainer(), ); const application = appContainer.get(Component.RestApplication); diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index 45a0ac4..48e109f 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -7,6 +7,7 @@ import {Component} from '../shared/types/index.js'; import {DatabaseClient} from '../shared/libs/database-client/index.js'; import {getMongoURI} from '../shared/helpers/index.js'; import {Controller, ExceptionFilter} from '../shared/libs/rest/index.js'; +import {ParseTokenMiddleware} from '../shared/libs/rest/middleware/parse-token.middleware.js'; @injectable() export class RestApplication { @@ -20,7 +21,7 @@ export class RestApplication { @inject(Component.ExceptionFilter) private readonly appExceptionFilter: ExceptionFilter, @inject(Component.UserController) private readonly userController: Controller, @inject(Component.OfferController) private readonly offerController: Controller, - + @inject(Component.AuthExceptionFilter) private readonly authExceptionFilter: ExceptionFilter, ) { this.server = express(); this.server.disable('x-powered-by'); @@ -50,14 +51,17 @@ export class RestApplication { } private async _initMiddleware() { + const authenticateMiddleware = new ParseTokenMiddleware(this.config.get('JWT_SECRET')); this.server.use(express.json()); this.server.use( '/upload', express.static(this.config.get('UPLOAD_DIRECTORY')) ); + this.server.use(authenticateMiddleware.execute.bind(authenticateMiddleware)); } private async _initExceptionFilters() { + this.server.use(this.authExceptionFilter.catch.bind(this.authExceptionFilter)); this.server.use(this.appExceptionFilter.catch.bind(this.appExceptionFilter)); } diff --git a/src/shared/libs/config/rest.schema.ts b/src/shared/libs/config/rest.schema.ts index e2687fa..b94d46b 100644 --- a/src/shared/libs/config/rest.schema.ts +++ b/src/shared/libs/config/rest.schema.ts @@ -12,6 +12,7 @@ export type RestSchema = { DB_PORT: string; DB_NAME: string; UPLOAD_DIRECTORY: string; + JWT_SECRET: string; } export const configRestSchema = convict({ @@ -63,6 +64,12 @@ export const configRestSchema = convict({ env: 'UPLOAD_DIRECTORY', default: 'upload' }, + JWT_SECRET: { + doc: 'Secret for sign JWT', + format: String, + env: 'JWT_SECRET', + default: 'ytfytfytf' + }, }); diff --git a/src/shared/libs/rest/middleware/parse-token.middleware.ts b/src/shared/libs/rest/middleware/parse-token.middleware.ts new file mode 100644 index 0000000..ed5b3af --- /dev/null +++ b/src/shared/libs/rest/middleware/parse-token.middleware.ts @@ -0,0 +1,46 @@ +import {NextFunction, Request, Response} from 'express'; +import {jwtVerify} from 'jose'; +import {StatusCodes} from 'http-status-codes'; +import {createSecretKey} from 'node:crypto'; +import {Middleware} from './middleware.interface.js'; +import {HttpError} from '../errors/index.js'; +import {TokenPayload} from '../../../modules/auth/index.js'; + +function isTokenPayload(payload: unknown): payload is TokenPayload { + return ( + (typeof payload === 'object' && payload !== null) && + ('email' in payload && typeof payload.email === 'string') && + ('name' in payload && typeof payload.name === 'string') && + ('avatarUrl' in payload && typeof payload.avatarUrl === 'string') && + ('isPro' in payload && typeof payload.isPro === 'boolean') && + ('id' in payload && typeof payload.id === 'string') + ); +} + +export class ParseTokenMiddleware implements Middleware { + constructor(private readonly jwtSecret: string) { + } + + public async execute(req: Request, _res: Response, next: NextFunction): Promise { + const authorizationHeader = req.headers?.authorization?.split(' '); + if (!authorizationHeader) { + return next(); + } + const [, token] = authorizationHeader; + try { + const {payload} = await jwtVerify(token, createSecretKey(this.jwtSecret, 'utf-8')); + if (isTokenPayload(payload)) { + req.tokenPayload = {...payload}; + return next(); + } else { + throw new Error('Bad token'); + } + } catch { + return next(new HttpError( + StatusCodes.UNAUTHORIZED, + 'Invalid token', + 'AuthenticateMiddleware') + ); + } + } +} diff --git a/src/shared/modules/auth/auth-service.interface.ts b/src/shared/modules/auth/auth-service.interface.ts new file mode 100644 index 0000000..095a5d7 --- /dev/null +++ b/src/shared/modules/auth/auth-service.interface.ts @@ -0,0 +1,5 @@ +import { LoginUserDto, UserEntity } from '../user/index.js'; +export interface AuthService { + authenticate(user: UserEntity): Promise; + verify(dto: LoginUserDto): Promise; +} diff --git a/src/shared/modules/auth/auth.constant.ts b/src/shared/modules/auth/auth.constant.ts new file mode 100644 index 0000000..269747d --- /dev/null +++ b/src/shared/modules/auth/auth.constant.ts @@ -0,0 +1,2 @@ +export const JWT_ALGORITHM = 'HS256'; +export const JWT_EXPIRED = '2d'; diff --git a/src/shared/modules/auth/auth.container.ts b/src/shared/modules/auth/auth.container.ts new file mode 100644 index 0000000..4bbca2b --- /dev/null +++ b/src/shared/modules/auth/auth.container.ts @@ -0,0 +1,12 @@ +import { Container } from 'inversify'; +import { AuthService } from './auth-service.interface.js'; +import { Component } from '../../types/index.js'; +import { DefaultAuthService } from './default-auth.service.js'; +import { ExceptionFilter } from '../../libs/rest/index.js'; +import { AuthExceptionFilter } from './auth.exception-filter.js'; +export function createAuthContainer() { + const authContainer = new Container(); + authContainer.bind(Component.AuthService).to(DefaultAuthService).inSingletonScope(); + authContainer.bind(Component.AuthExceptionFilter).to(AuthExceptionFilter).inSingletonScope(); + return authContainer; +} diff --git a/src/shared/modules/auth/auth.exception-filter.ts b/src/shared/modules/auth/auth.exception-filter.ts new file mode 100644 index 0000000..6bb5636 --- /dev/null +++ b/src/shared/modules/auth/auth.exception-filter.ts @@ -0,0 +1,25 @@ +import { inject, injectable } from 'inversify'; +import { NextFunction, Request, Response } from 'express'; +import { ExceptionFilter } from '../../libs/rest/index.js'; +import { Component } from '../../types/index.js'; +import { Logger } from '../../libs/logger/index.js'; +import { BaseUserException } from './errors/index.js'; +@injectable() +export class AuthExceptionFilter implements ExceptionFilter { + constructor( + @inject(Component.Logger) private readonly logger: Logger + ) { + this.logger.info('Register AuthExceptionFilter'); + } + public catch(error: unknown, _req: Request, res: Response, next: NextFunction): void { + if (! (error instanceof BaseUserException)) { + return next(error); + } + this.logger.error(`[AuthModule] ${error.message}`, error); + res.status(error.httpStatusCode) + .json({ + type: 'AUTHORIZATION', + error: error.message, + }); + } +} diff --git a/src/shared/modules/auth/default-auth.service.ts b/src/shared/modules/auth/default-auth.service.ts new file mode 100644 index 0000000..1cf14d3 --- /dev/null +++ b/src/shared/modules/auth/default-auth.service.ts @@ -0,0 +1,52 @@ +import {inject, injectable} from 'inversify'; +import * as crypto from 'node:crypto'; +import {SignJWT} from 'jose'; +import {AuthService} from './auth-service.interface.js'; +import {Component} from '../../types/index.js'; +import {Logger} from '../../libs/logger/index.js'; +import {LoginUserDto, UserEntity, UserService} from '../user/index.js'; +import {TokenPayload} from './types/TokenPayload.js'; +import {Config, RestSchema} from '../../libs/config/index.js'; +import {UserNotFoundException, UserPasswordIncorrectException} from './errors/index.js'; +import {JWT_ALGORITHM, JWT_EXPIRED} from './auth.constant.js'; + +@injectable() +export class DefaultAuthService implements AuthService { + constructor( + @inject(Component.Logger) private readonly logger: Logger, + @inject(Component.UserService) private readonly userService: UserService, + @inject(Component.Config) private readonly config: Config, + ) { + } + + public async authenticate(user: UserEntity): Promise { + const jwtSecret = this.config.get('JWT_SECRET'); + const secretKey = crypto.createSecretKey(jwtSecret, 'utf-8'); + const tokenPayload: TokenPayload = { + email: user.email, + name: user.name, + avatarUrl: user.avatarUrl, + isPro: user.isPro, + id: user.id, + }; + this.logger.info(`Create token for ${user.email}`); + return new SignJWT(tokenPayload) + .setProtectedHeader({alg: JWT_ALGORITHM}) + .setIssuedAt() + .setExpirationTime(JWT_EXPIRED) + .sign(secretKey); + } + + public async verify(dto: LoginUserDto): Promise { + const user = await this.userService.findByEmail(dto.email); + if (!user) { + this.logger.warn(`User with ${dto.email} not found`); + throw new UserNotFoundException(); + } + if (!user.verifyPassword(dto.password, this.config.get('SALT'))) { + this.logger.warn(`Incorrect password for ${dto.email}`); + throw new UserPasswordIncorrectException(); + } + return user; + } +} diff --git a/src/shared/modules/auth/errors/base-user.exception.ts b/src/shared/modules/auth/errors/base-user.exception.ts new file mode 100644 index 0000000..a6752d4 --- /dev/null +++ b/src/shared/modules/auth/errors/base-user.exception.ts @@ -0,0 +1,6 @@ +import { HttpError } from '../../../libs/rest/index.js'; +export class BaseUserException extends HttpError { + constructor(httpStatusCode: number, message: string) { + super(httpStatusCode, message); + } +} diff --git a/src/shared/modules/auth/errors/index.ts b/src/shared/modules/auth/errors/index.ts new file mode 100644 index 0000000..349bcb5 --- /dev/null +++ b/src/shared/modules/auth/errors/index.ts @@ -0,0 +1,3 @@ +export * from './user-not-found.exeption.js'; +export * from './base-user.exception.js'; +export * from './user-password-incorrect.exeption.js'; diff --git a/src/shared/modules/auth/errors/user-not-found.exeption.ts b/src/shared/modules/auth/errors/user-not-found.exeption.ts new file mode 100644 index 0000000..97c73f8 --- /dev/null +++ b/src/shared/modules/auth/errors/user-not-found.exeption.ts @@ -0,0 +1,7 @@ +import { StatusCodes } from 'http-status-codes'; +import { BaseUserException } from './base-user.exception.js'; +export class UserNotFoundException extends BaseUserException { + constructor() { + super(StatusCodes.NOT_FOUND, 'User not found'); + } +} diff --git a/src/shared/modules/auth/errors/user-password-incorrect.exeption.ts b/src/shared/modules/auth/errors/user-password-incorrect.exeption.ts new file mode 100644 index 0000000..cefbf35 --- /dev/null +++ b/src/shared/modules/auth/errors/user-password-incorrect.exeption.ts @@ -0,0 +1,7 @@ +import { StatusCodes } from 'http-status-codes'; +import { BaseUserException } from './base-user.exception.js'; +export class UserPasswordIncorrectException extends BaseUserException { + constructor() { + super(StatusCodes.UNAUTHORIZED, 'Incorrect user name or password'); + } +} diff --git a/src/shared/modules/auth/index.ts b/src/shared/modules/auth/index.ts new file mode 100644 index 0000000..72d9044 --- /dev/null +++ b/src/shared/modules/auth/index.ts @@ -0,0 +1,4 @@ +export * from './auth-service.interface.js'; +export * from './types/TokenPayload.js'; +export * from './auth.container.js'; +export * from './default-auth.service.js'; diff --git a/src/shared/modules/auth/types/TokenPayload.ts b/src/shared/modules/auth/types/TokenPayload.ts new file mode 100644 index 0000000..347fbb9 --- /dev/null +++ b/src/shared/modules/auth/types/TokenPayload.ts @@ -0,0 +1,7 @@ +export type TokenPayload = { + email: string; + name: string; + avatarUrl: string; + isPro: boolean; + id: string; +}; diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index a1338e3..358032f 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -47,10 +47,10 @@ export class CommentController extends BaseController { } public async create( - {body}: Request, Record, CreateCommentDto>, + {body, tokenPayload}: Request, Record, CreateCommentDto>, res: Response ): Promise { - const comment = await this.commentService.create(body); + const comment = await this.commentService.create({ ...body, authorId: tokenPayload.id }); this.created(res, fillDTO(CommentRdo, comment)); } } diff --git a/src/shared/modules/comment/dto/create-comment.dto.ts b/src/shared/modules/comment/dto/create-comment.dto.ts index 6f04760..9a2cf5d 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -13,7 +13,6 @@ export class CreateCommentDto { @Max(5, {message: CreateCommentValidationMessages.rating.maxValue}) public rating: number; - @IsMongoId({message: CreateCommentValidationMessages.offerId.invalidId}) public authorId: string; @IsMongoId({message: CreateCommentValidationMessages.offerId.invalidId}) diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index b604098..eb0df5d 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -4,4 +4,5 @@ export * from './default-user.service.js'; export * from './user.container.js'; export * from './user-service.interface.js'; export * from './user.controller.js'; - +export * from './dto/login-user.dto.js'; +export * from './user-service.interface.js'; diff --git a/src/shared/modules/user/rdo/logged-user.rdo.ts b/src/shared/modules/user/rdo/logged-user.rdo.ts new file mode 100644 index 0000000..2459326 --- /dev/null +++ b/src/shared/modules/user/rdo/logged-user.rdo.ts @@ -0,0 +1,7 @@ +import { Expose } from 'class-transformer'; +export class LoggedUserRdo { + @Expose() + public token: string; + @Expose() + public email: string; +} diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index a430918..e686ef1 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -18,6 +18,8 @@ import {UserRdo} from './rdo/user.rdo.js'; import {LoginUserRequest} from './login-user-request.type.js'; import {CreateUserDto} from './dto/create-user.dto.js'; import {LoginUserDto} from './dto/login-user.dto.js'; +import {LoggedUserRdo} from './rdo/logged-user.rdo.js'; +import {AuthService} from '../auth/index.js'; @injectable() export class UserController extends BaseController { @@ -25,6 +27,7 @@ export class UserController extends BaseController { @inject(Component.Logger) protected readonly logger: Logger, @inject(Component.UserService) private readonly userService: UserService, @inject(Component.Config) private readonly configService: Config, + @inject(Component.AuthService) private readonly authService: AuthService, ) { super(logger); this.logger.info('Register routes for UserController...'); @@ -73,23 +76,15 @@ export class UserController extends BaseController { public async login( {body}: LoginUserRequest, - _res: Response, + res: Response, ): Promise { - const existsUser = await this.userService.findByEmail(body.email); - - if (!existsUser) { - throw new HttpError( - StatusCodes.UNAUTHORIZED, - `User with email ${body.email} not found.`, - 'UserController', - ); - } - - throw new HttpError( - StatusCodes.NOT_IMPLEMENTED, - 'Not implemented', - 'UserController', - ); + const user = await this.authService.verify(body); + const token = await this.authService.authenticate(user); + const responseData = fillDTO(LoggedUserRdo, { + email: user.email, + token, + }); + this.ok(res, responseData); } public async uploadAvatar(req: Request, res: Response) { diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index a952961..614090c 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -27,9 +27,6 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { @prop({required: true, unique: true, type: () => String}) public email: string; - @prop({required: true, type: () => String}) - public token: string; - @prop({required: true, default: '', type: () => String}) private password?: string; @@ -40,7 +37,6 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { this.avatarUrl = userData.avatarUrl; this.isPro = userData.isPro; this.email = userData.email; - this.token = userData.token; } public setPassword(password: string, salt: string) { @@ -50,6 +46,11 @@ export class UserEntity extends defaultClasses.TimeStamps implements User { public getPassword() { return this.password; } + + public verifyPassword(password: string, salt: string) { + const hashPassword = createSHA256(password, salt); + return hashPassword === this.password; + } } export const UserModel = getModelForClass(UserEntity); diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index b7110b9..a7258f8 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -13,4 +13,6 @@ export const Component = { ExceptionFilter: Symbol.for('ExceptionFilter'), UserController: Symbol.for('UserController'), OfferController: Symbol.for('OfferController'), + AuthService: Symbol.for('AuthService'), + AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), } as const; diff --git a/src/shared/types/user.type.ts b/src/shared/types/user.type.ts index a34be20..d4ba289 100644 --- a/src/shared/types/user.type.ts +++ b/src/shared/types/user.type.ts @@ -3,5 +3,4 @@ export type User = { avatarUrl: string; isPro: boolean; email: string; - token: string; }; diff --git a/tsconfig.json b/tsconfig.json index dbd5167..2e5cfb4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,5 +37,6 @@ ], "ts-node": { "esm": true - } + }, + "files": ["./custom.d.ts"] } From 74f30614757ebb53cc4fcb23f4cb8c48fc5499a0 Mon Sep 17 00:00:00 2001 From: AdonaiJehosua Date: Tue, 19 Nov 2024 22:16:41 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=D0=A1=D0=BE=D0=B7=D0=B4=D0=B0=D1=81=D1=82?= =?UTF-8?q?=20middleware=20"PrivateRouteMiddleware"=20=D0=B4=D0=BB=D1=8F?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=BD=D0=B0=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20=D0=BD=D0=B0=20=D0=BE=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=BD=D1=83=D1=8E=20?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83,=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B8=D1=82=20=D0=B5=D0=B5?= =?UTF-8?q?=20=D0=B2=20=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D1=85=20=D0=BC=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=85.=20=D0=98=D0=B7=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20login=20=D0=B2?= =?UTF-8?q?=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81=D0=B5=20UserController?= =?UTF-8?q?=20=D1=81=20=D1=83=D1=87=D0=B5=D1=82=D0=BE=D0=BC=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=88=D0=B5=D0=B3=D0=BE=20=D1=81=D0=B5=D1=80=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B8=20=D0=B8=D0=B7=20=D0=BF=D1=80=D0=B5=D0=B4=D1=8B?= =?UTF-8?q?=D0=B4=D1=83=D1=89=D0=B5=D0=B3=D0=BE=20=D1=88=D0=B0=D0=B3=D0=B0?= =?UTF-8?q?.=20=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D1=83=D0=B5=D1=82=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B5=D0=B4=D0=B5=D0=BD=D0=B8=D0=B5,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D0=BE=D0=BC?= =?UTF-8?q?=20=D0=B4=D0=BB=D1=8F=20=D0=BD=D0=B5=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D1=8F=20=D0=BE=D0=B1=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=81=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B8=D1=85=D0=BE=D0=B4=D1=8F=D1=82=20=D1=81=20=D0=B2?= =?UTF-8?q?=D0=BB=D0=B0=D0=B3=D0=BE=D0=BC=20isFavorite=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BD=D1=8B=D0=BC=20false.=20=D0=98=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=82=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0?= =?UTF-8?q?=D0=B9=D0=B4=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B3=D0=BE=20=D0=BE=D0=B1?= =?UTF-8?q?=D1=8A=D1=8F=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F.=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=20=D0=B2=20offerService=20?= =?UTF-8?q?=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20exist.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/libs/rest/index.ts | 1 + .../middleware/private-route.middleware.ts | 16 +++++++++ .../modules/comment/comment.controller.ts | 16 ++++++--- .../modules/offer/dto/create-offer.dto.ts | 3 +- src/shared/modules/offer/offer.controller.ts | 34 ++++++++++++++----- src/shared/modules/offer/offer.http | 2 ++ src/shared/modules/offer/offer.service.ts | 5 +++ .../modules/user/dto/create-user.dto.ts | 4 +-- src/shared/modules/user/user.controller.ts | 16 +++++++++ src/shared/modules/user/user.http | 12 +++++-- 10 files changed, 91 insertions(+), 18 deletions(-) create mode 100644 src/shared/libs/rest/middleware/private-route.middleware.ts diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index 44e62be..3afabe3 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -12,3 +12,4 @@ export * from './middleware/validate-objectid.middleware.js'; export * from './middleware/validate-dto.middleware.js'; export * from './middleware/document-exista.middleware.js'; export * from './middleware/upload-file.middleware.js'; +export * from './middleware/private-route.middleware.js'; diff --git a/src/shared/libs/rest/middleware/private-route.middleware.ts b/src/shared/libs/rest/middleware/private-route.middleware.ts new file mode 100644 index 0000000..511611e --- /dev/null +++ b/src/shared/libs/rest/middleware/private-route.middleware.ts @@ -0,0 +1,16 @@ +import { StatusCodes } from 'http-status-codes'; +import { NextFunction, Request, Response } from 'express'; +import { Middleware } from './middleware.interface.js'; +import { HttpError } from '../errors/index.js'; +export class PrivateRouteMiddleware implements Middleware { + public async execute({ tokenPayload }: Request, _res: Response, next: NextFunction): Promise { + if (! tokenPayload) { + throw new HttpError( + StatusCodes.UNAUTHORIZED, + 'Unauthorized', + 'PrivateRouteMiddleware' + ); + } + return next(); + } +} diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index 358032f..2c65089 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -3,7 +3,7 @@ import {Request, Response} from 'express'; import { BaseController, DocumentExistsMiddleware, - HttpMethod, + HttpMethod, PrivateRouteMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; @@ -30,13 +30,21 @@ export class CommentController extends BaseController { path: '/:offerId', method: HttpMethod.Get, handler: this.index, - middlewares: [new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),] + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') + ] }); this.addRoute({ path: '/:offerId', method: HttpMethod.Post, handler: this.create, - middlewares: [new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(CreateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId')] + middlewares: [ + new ValidateObjectIdMiddleware('offerId'), + new ValidateDtoMiddleware(CreateOfferDto), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'), + new PrivateRouteMiddleware(), + ] }); } @@ -50,7 +58,7 @@ export class CommentController extends BaseController { {body, tokenPayload}: Request, Record, CreateCommentDto>, res: Response ): Promise { - const comment = await this.commentService.create({ ...body, authorId: tokenPayload.id }); + const comment = await this.commentService.create({...body, authorId: tokenPayload.id}); this.created(res, fillDTO(CommentRdo, comment)); } } diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index 0ce7cb6..faaf1ec 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -5,7 +5,7 @@ import { IsArray, IsBoolean, IsEnum, - IsInt, IsMongoId, + IsInt, IsNumber, IsString, Max, MaxLength, @@ -71,7 +71,6 @@ export class CreateOfferDto { @ArrayUnique({message: CreateOfferValidationMessage.offerGood.unique}) public goods: OfferGood[]; - @IsMongoId({message: CreateOfferValidationMessage.hostId.invalidId}) public hostId: string; @IsInt({message: CreateOfferValidationMessage.bedrooms.invalidFormat}) diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 74da2ca..27e8772 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -2,7 +2,7 @@ import {inject, injectable} from 'inversify'; import { BaseController, DocumentExistsMiddleware, - HttpMethod, + HttpMethod, PrivateRouteMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware } from '../../libs/rest/index.js'; @@ -38,7 +38,10 @@ export class OfferController extends BaseController { path: '/', method: HttpMethod.Post, handler: this.create, - middlewares: [new ValidateDtoMiddleware((CreateOfferDto))] + middlewares: [ + new ValidateDtoMiddleware((CreateOfferDto)), + new PrivateRouteMiddleware() + ] }); this.addRoute({ path: '/premium', @@ -51,7 +54,10 @@ export class OfferController extends BaseController { path: '/favorites', method: HttpMethod.Patch, handler: this.changeFavoriteStatus, - middlewares: [new ValidateDtoMiddleware(ChangeFavoriteStatusDto)] + middlewares: [ + new ValidateDtoMiddleware(ChangeFavoriteStatusDto), + new PrivateRouteMiddleware() + ] }); this.addRoute({ path: '/:id', @@ -63,29 +69,41 @@ export class OfferController extends BaseController { path: '/:id', method: HttpMethod.Patch, handler: this.edit, - middlewares: [new ValidateObjectIdMiddleware('id'), new ValidateDtoMiddleware(UpdateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'id')] + middlewares: [ + new ValidateObjectIdMiddleware('id'), + new ValidateDtoMiddleware(UpdateOfferDto), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'id'), + new PrivateRouteMiddleware() + ] }); this.addRoute({ path: '/:id', method: HttpMethod.Delete, handler: this.delete, - middlewares: [new ValidateObjectIdMiddleware('id'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'id'),] + middlewares: [ + new ValidateObjectIdMiddleware('id'), + new DocumentExistsMiddleware(this.offerService, 'Offer', 'id'), + new PrivateRouteMiddleware() + ] }); } public async index( - _req: Request, + {tokenPayload}: Request, res: Response, ): Promise { const result = await this.offerService.findAll(); + if (!tokenPayload) { + result.map((item) => (item.isFavorite = false)); + } this.ok(res, result); } public async create( - {body}: CreateOfferRequest, + {body, tokenPayload}: CreateOfferRequest, res: Response, ): Promise { - const result = await this.offerService.create(body); + const result = await this.offerService.create({...body, hostId: tokenPayload.id}); this.created(res, fillDTO(OfferRdo, result)); } diff --git a/src/shared/modules/offer/offer.http b/src/shared/modules/offer/offer.http index 31ec5a0..809ad28 100644 --- a/src/shared/modules/offer/offer.http +++ b/src/shared/modules/offer/offer.http @@ -1,5 +1,7 @@ GET http://localhost:4000/offers HTTP/1.1 Content-Type: application/json +#Authorization: Bearer eyJhbGciOJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJuYW1lIjoiU2VyaW9nYSIsImF2YXRhclVybCI6InRvcnJhbmNlLnBuZyIsImlzUHJvIjp0cnVlLCJpZCI6IjY3M2NiNGIyM2UxNGY4ODVhZmQxOTNmNiIsImlhdCI6MTczMjAzMTcxOSwiZXhwIjoxNzMyMjA0NTE5fQ.G1nNU5Vfy9V_PMINU2S4Qqb1szITjSegR0WfFWcmOcw + ### diff --git a/src/shared/modules/offer/offer.service.ts b/src/shared/modules/offer/offer.service.ts index 066c2c1..1746d63 100644 --- a/src/shared/modules/offer/offer.service.ts +++ b/src/shared/modules/offer/offer.service.ts @@ -18,6 +18,11 @@ export class OfferService implements OfferServiceInterface { ) { } + public async exists(documentId: string): Promise { + return (await this.offerModel + .exists({_id: documentId})) !== null; + } + public async create(dto: CreateOfferDto): Promise> { dto.commentsCount = 0; const result = await this.offerModel diff --git a/src/shared/modules/user/dto/create-user.dto.ts b/src/shared/modules/user/dto/create-user.dto.ts index e08a85a..0830dd1 100644 --- a/src/shared/modules/user/dto/create-user.dto.ts +++ b/src/shared/modules/user/dto/create-user.dto.ts @@ -4,8 +4,8 @@ import {CreateUserValidationMessages} from './create-user.messages.js'; export class CreateUserDto { @IsString({message: CreateUserValidationMessages.name.invalidFormat}) - @MinLength(10, {message: CreateUserValidationMessages.name.minLength}) - @MaxLength(100, {message: CreateUserValidationMessages.name.maxLength}) + @MinLength(1, {message: CreateUserValidationMessages.name.minLength}) + @MaxLength(15, {message: CreateUserValidationMessages.name.maxLength}) public name: string; @IsString({message: CreateUserValidationMessages.avatarUrl.invalidFormat}) diff --git a/src/shared/modules/user/user.controller.ts b/src/shared/modules/user/user.controller.ts index e686ef1..0d44b81 100644 --- a/src/shared/modules/user/user.controller.ts +++ b/src/shared/modules/user/user.controller.ts @@ -38,6 +38,11 @@ export class UserController extends BaseController { handler: this.create, middlewares: [new ValidateDtoMiddleware(CreateUserDto)] }); + this.addRoute({ + path: '/login', + method: HttpMethod.Get, + handler: this.checkAuthenticate, + }); this.addRoute({ path: '/login', method: HttpMethod.Post, @@ -73,6 +78,17 @@ export class UserController extends BaseController { this.created(res, fillDTO(UserRdo, result)); } + public async checkAuthenticate({ tokenPayload: { email }}: Request, res: Response) { + const foundedUser = await this.userService.findByEmail(email); + if (! foundedUser) { + throw new HttpError( + StatusCodes.UNAUTHORIZED, + 'Unauthorized', + 'UserController' + ); + } + this.ok(res, fillDTO(LoggedUserRdo, foundedUser)); + } public async login( {body}: LoginUserRequest, diff --git a/src/shared/modules/user/user.http b/src/shared/modules/user/user.http index c6d5085..bd11959 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -14,9 +14,8 @@ Content-Type: application/json { "name": "Serioga", "avatarUrl": "torrance.png", -"isPro": "true", +"isPro": true, "email": "torans@overlook.net", -"token": "sdrfgsdrhgg3456sdg", "password": "shindrtryredtyerting" } @@ -34,3 +33,12 @@ Content-Type: image/jpg < /Users/sergeiragozin/Pictures/Screenshots/2150932060.jpg ------WebKitFormBoundary7MA4YWxkTrZu0gW-- ## + +### + +## Проверить токен пользователя + +GET http://localhost:4000/users/login HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJuYW1lIjoiU2VyaW9nYSIsImF2YXRhclVybCI6InRvcnJhbmNlLnBuZyIsImlzUHJvIjp0cnVlLCJpZCI6IjY3M2NiNGIyM2UxNGY4ODVhZmQxOTNmNiIsImlhdCI6MTczMjAzMTcxOSwiZXhwIjoxNzMyMjA0NTE5fQ.G1nNU5Vfy9V_PMINU2S4Qqb1szITjSegR0WfFWcmOcw + +### From 3045850751c69056714ce155e4f9a31135315413 Mon Sep 17 00:00:00 2001 From: AdonaiJehosua Date: Thu, 21 Nov 2024 21:00:45 +0500 Subject: [PATCH 3/3] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BD=D0=BE=D1=81=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/modules/auth/auth.exception-filter.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/shared/modules/auth/auth.exception-filter.ts b/src/shared/modules/auth/auth.exception-filter.ts index 6bb5636..1aee56f 100644 --- a/src/shared/modules/auth/auth.exception-filter.ts +++ b/src/shared/modules/auth/auth.exception-filter.ts @@ -1,9 +1,10 @@ -import { inject, injectable } from 'inversify'; -import { NextFunction, Request, Response } from 'express'; -import { ExceptionFilter } from '../../libs/rest/index.js'; -import { Component } from '../../types/index.js'; -import { Logger } from '../../libs/logger/index.js'; -import { BaseUserException } from './errors/index.js'; +import {inject, injectable} from 'inversify'; +import {NextFunction, Request, Response} from 'express'; +import {ExceptionFilter} from '../../libs/rest/index.js'; +import {Component} from '../../types/index.js'; +import {Logger} from '../../libs/logger/index.js'; +import {BaseUserException} from './errors/index.js'; + @injectable() export class AuthExceptionFilter implements ExceptionFilter { constructor( @@ -11,8 +12,9 @@ export class AuthExceptionFilter implements ExceptionFilter { ) { this.logger.info('Register AuthExceptionFilter'); } + public catch(error: unknown, _req: Request, res: Response, next: NextFunction): void { - if (! (error instanceof BaseUserException)) { + if (!(error instanceof BaseUserException)) { return next(error); } this.logger.error(`[AuthModule] ${error.message}`, error);