diff --git a/custom.d.ts b/custom.d.ts new file mode 100644 index 0000000..778a665 --- /dev/null +++ b/custom.d.ts @@ -0,0 +1,7 @@ +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 9b2b249..cc5ae68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,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", @@ -4192,6 +4193,14 @@ "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", "dev": true }, + "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", @@ -10202,6 +10211,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 e8e13d4..435ccee 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,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 734e0d5..f2a0693 100644 --- a/src/main.rest.ts +++ b/src/main.rest.ts @@ -7,6 +7,7 @@ import { createUserContainer } from './shared/modules/user/index.js'; import { createCategoryContainer } from './shared/modules/category/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() { @@ -16,6 +17,7 @@ async function bootstrap() { createCategoryContainer(), createOfferContainer(), createCommentContainer(), + createAuthContainer(), ); const application = appContainer.get(Component.RestApplication); diff --git a/src/rest/rest.application.ts b/src/rest/rest.application.ts index b151c72..2e574e3 100644 --- a/src/rest/rest.application.ts +++ b/src/rest/rest.application.ts @@ -6,6 +6,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,6 +21,7 @@ export class RestApplication { @inject(Component.UserController) private readonly userController: Controller, @inject(Component.OfferController) private readonly offerController: Controller, @inject(Component.CommentController) private readonly commentController: Controller, + @inject(Component.AuthExceptionFilter) private readonly authExceptionFilter: ExceptionFilter, ) { this.server = express(); } @@ -49,14 +51,18 @@ 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 b5f038f..4bdb0e0 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,4 +64,10 @@ export const configRestSchema = convict({ env: 'UPLOAD_DIRECTORY', default: null }, + JWT_SECRET: { + doc: 'Secret for sign JWT', + format: String, + env: 'JWT_SECRET', + default: null + }, }); diff --git a/src/shared/libs/rest/index.ts b/src/shared/libs/rest/index.ts index f6ca54a..9f1da53 100644 --- a/src/shared/libs/rest/index.ts +++ b/src/shared/libs/rest/index.ts @@ -13,3 +13,5 @@ export * from './middleware/validate-objectid.middleware.js'; export * from './middleware/validate-dto.middleware.js'; export * from './middleware/document-exists.middleware.js'; export * from './middleware/upload-file.middleware.js'; +export * from './middleware/parse-token.middleware.js'; +export * from './middleware/private-route.middleware.js'; 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..89af22a --- /dev/null +++ b/src/shared/libs/rest/middleware/parse-token.middleware.ts @@ -0,0 +1,48 @@ +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') && + ('firstname' in payload && typeof payload.firstname === 'string') && + ('lastname' in payload && typeof payload.lastname === 'string') && + ('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/libs/rest/middleware/private-route.middleware.ts b/src/shared/libs/rest/middleware/private-route.middleware.ts new file mode 100644 index 0000000..3c73a91 --- /dev/null +++ b/src/shared/libs/rest/middleware/private-route.middleware.ts @@ -0,0 +1,18 @@ +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/auth/auth-service.interface.ts b/src/shared/modules/auth/auth-service.interface.ts new file mode 100644 index 0000000..9d899c5 --- /dev/null +++ b/src/shared/modules/auth/auth-service.interface.ts @@ -0,0 +1,6 @@ +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..9c83d63 --- /dev/null +++ b/src/shared/modules/auth/auth.container.ts @@ -0,0 +1,14 @@ +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..500a134 --- /dev/null +++ b/src/shared/modules/auth/auth.exception-filter.ts @@ -0,0 +1,28 @@ +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..b786ddc --- /dev/null +++ b/src/shared/modules/auth/default-auth.service.ts @@ -0,0 +1,55 @@ +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, + firstname: user.firstname, + lastname: user.lastname, + 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..27f8f1b --- /dev/null +++ b/src/shared/modules/auth/errors/base-user.exception.ts @@ -0,0 +1,7 @@ +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..e1154ae --- /dev/null +++ b/src/shared/modules/auth/errors/index.ts @@ -0,0 +1,3 @@ +export * from './base-user.exception.js'; +export * from './user-not-found.exception.js'; +export * from './user-password-incorrect.exception.js'; diff --git a/src/shared/modules/auth/errors/user-not-found.exception.ts b/src/shared/modules/auth/errors/user-not-found.exception.ts new file mode 100644 index 0000000..d5833d0 --- /dev/null +++ b/src/shared/modules/auth/errors/user-not-found.exception.ts @@ -0,0 +1,8 @@ +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.exception.ts b/src/shared/modules/auth/errors/user-password-incorrect.exception.ts new file mode 100644 index 0000000..8579750 --- /dev/null +++ b/src/shared/modules/auth/errors/user-password-incorrect.exception.ts @@ -0,0 +1,8 @@ +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..b4c88cb --- /dev/null +++ b/src/shared/modules/auth/types/TokenPayload.ts @@ -0,0 +1,6 @@ +export type TokenPayload = { + email: string; + firstname: string; + lastname: string; + id: string; +}; diff --git a/src/shared/modules/category/category.controller.ts b/src/shared/modules/category/category.controller.ts index 1fd1698..c71fcd5 100644 --- a/src/shared/modules/category/category.controller.ts +++ b/src/shared/modules/category/category.controller.ts @@ -3,7 +3,7 @@ import { Response, Request } from 'express'; import { BaseController, HttpError, - HttpMethod, + HttpMethod, PrivateRouteMiddleware, RequestQuery, ValidateDtoMiddleware, ValidateObjectIdMiddleware, } from '../../libs/rest/index.js'; @@ -33,7 +33,10 @@ export class CategoryController extends BaseController { path: '/', method: HttpMethod.Post, handler: this.create, - middlewares: [new ValidateDtoMiddleware(CreateCategoryDto)] + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateDtoMiddleware(CreateCategoryDto) + ] }); this.addRoute({ path: '/:categoryId/offers', diff --git a/src/shared/modules/comment/comment.controller.ts b/src/shared/modules/comment/comment.controller.ts index e2e4f7b..8efdcc3 100644 --- a/src/shared/modules/comment/comment.controller.ts +++ b/src/shared/modules/comment/comment.controller.ts @@ -1,7 +1,13 @@ import { inject, injectable } from 'inversify'; import { Response } from 'express'; import { StatusCodes } from 'http-status-codes'; -import { BaseController, HttpError, HttpMethod, ValidateDtoMiddleware } from '../../libs/rest/index.js'; +import { + BaseController, + HttpError, + HttpMethod, + PrivateRouteMiddleware, + ValidateDtoMiddleware, +} from '../../libs/rest/index.js'; import { Component } from '../../types/index.js'; import { Logger } from '../../libs/logger/index.js'; import { CommentService } from './comment-service.interface.js'; @@ -26,16 +32,16 @@ export default class CommentController extends BaseController { method: HttpMethod.Post, handler: this.create, middlewares: [ + new PrivateRouteMiddleware(), new ValidateDtoMiddleware(CreateCommentDto) ] }); } public async create( - { body }: CreateCommentRequest, + { body, tokenPayload }: CreateCommentRequest, res: Response ): Promise { - if (! await this.offerService.exists(body.offerId)) { throw new HttpError( StatusCodes.NOT_FOUND, @@ -44,7 +50,7 @@ export default class CommentController extends BaseController { ); } - const comment = await this.commentService.create(body); + const comment = await this.commentService.create({ ...body, userId: tokenPayload.id }); await this.offerService.incCommentCount(body.offerId); 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 39843ff..7a7dbd1 100644 --- a/src/shared/modules/comment/dto/create-comment.dto.ts +++ b/src/shared/modules/comment/dto/create-comment.dto.ts @@ -9,6 +9,5 @@ export class CreateCommentDto { @IsMongoId({ message: CreateCommentMessages.offerId.invalidFormat }) public offerId: string; - @IsMongoId({ message: CreateCommentMessages.userId.invalidFormat }) public userId: string; } diff --git a/src/shared/modules/offer/dto/create-offer.dto.ts b/src/shared/modules/offer/dto/create-offer.dto.ts index e53c85c..0fc885e 100644 --- a/src/shared/modules/offer/dto/create-offer.dto.ts +++ b/src/shared/modules/offer/dto/create-offer.dto.ts @@ -29,6 +29,5 @@ export class CreateOfferDto { @IsMongoId({ each: true, message: CreateOfferValidationMessage.categories.invalidId }) public categories: string[]; - @IsMongoId({ message: CreateOfferValidationMessage.userId.invalidId }) public userId: string; } diff --git a/src/shared/modules/offer/offer.controller.ts b/src/shared/modules/offer/offer.controller.ts index 994f23c..42d1c74 100644 --- a/src/shared/modules/offer/offer.controller.ts +++ b/src/shared/modules/offer/offer.controller.ts @@ -1,6 +1,6 @@ import { BaseController, DocumentExistsMiddleware, - HttpMethod, + HttpMethod, PrivateRouteMiddleware, ValidateDtoMiddleware, ValidateObjectIdMiddleware, } from '../../libs/rest/index.js'; @@ -42,13 +42,17 @@ export default class OfferController extends BaseController { path: '/', method: HttpMethod.Post, handler: this.create, - middlewares: [new ValidateDtoMiddleware(CreateOfferDto)] + middlewares: [ + new PrivateRouteMiddleware(), + new ValidateDtoMiddleware(CreateOfferDto) + ] }); this.addRoute({ path: '/:offerId', method: HttpMethod.Delete, handler: this.delete, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') ] @@ -58,6 +62,7 @@ export default class OfferController extends BaseController { method: HttpMethod.Patch, handler: this.update, middlewares: [ + new PrivateRouteMiddleware(), new ValidateObjectIdMiddleware('offerId'), new ValidateDtoMiddleware(UpdateOfferDto), new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId') @@ -87,8 +92,8 @@ export default class OfferController extends BaseController { this.ok(res, fillDTO(OfferRdo, offers)); } - public async create({ body }: CreateOfferRequest, res: Response): Promise { - const result = await this.offerService.create(body); + public async create({ body, tokenPayload }: CreateOfferRequest, res: Response): Promise { + const result = await this.offerService.create({ ...body, userId: tokenPayload.id }); const offer = await this.offerService.findById(result.id); this.created(res, fillDTO(OfferRdo, offer)); } diff --git a/src/shared/modules/user/index.ts b/src/shared/modules/user/index.ts index 708a8aa..a9e658f 100644 --- a/src/shared/modules/user/index.ts +++ b/src/shared/modules/user/index.ts @@ -3,3 +3,5 @@ export * from './dto/create-user.dto.js'; export * from './default-user.service.js'; export * from './user.container.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..543bd7d --- /dev/null +++ b/src/shared/modules/user/rdo/logged-user.rdo.ts @@ -0,0 +1,9 @@ +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 a2f7ee8..13c4291 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 { AuthService } from '../auth/index.js'; +import { LoggedUserRdo } from './rdo/logged-user.rdo.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…'); @@ -50,6 +53,11 @@ export class UserController extends BaseController { new UploadFileMiddleware(this.configService.get('UPLOAD_DIRECTORY'), 'avatar'), ] }); + this.addRoute({ + path: '/login', + method: HttpMethod.Get, + handler: this.checkAuthenticate, + }); } public async create( @@ -72,23 +80,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) { @@ -96,4 +96,18 @@ export class UserController extends BaseController { filepath: req.file?.path }); } + + 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)); + } } diff --git a/src/shared/modules/user/user.entity.ts b/src/shared/modules/user/user.entity.ts index 1618bfd..2bde8b2 100644 --- a/src/shared/modules/user/user.entity.ts +++ b/src/shared/modules/user/user.entity.ts @@ -44,6 +44,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/modules/user/user.http b/src/shared/modules/user/user.http index bd6a061..e8c8ea4 100644 --- a/src/shared/modules/user/user.http +++ b/src/shared/modules/user/user.http @@ -38,4 +38,10 @@ Content-Type: image/png < /Users/spider_net/Desktop/screen.png ------WebKitFormBoundary7MA4YWxkTrZu0gW-- -## +### + +## Проверить токен пользователя +GET http://localhost:4000/users/login HTTP/1.1 +Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6InRvcmFuc0BvdmVybG9vay5uZXQiLCJpZCI6IjY0NzBiYzM4M2UzMGRiNjc5ZGZhYzJkMiIsImlhdCI6MTY4NTk3NTQ4MiwiZXhwIjoxNjg2MTQ4MjgyfQ.Gq2-B1egouAnMxmXlR2ElVT6wCa97PS6lxzVI8LnGvo + +### diff --git a/src/shared/types/component.enum.ts b/src/shared/types/component.enum.ts index 1343b47..2c17bb2 100644 --- a/src/shared/types/component.enum.ts +++ b/src/shared/types/component.enum.ts @@ -16,4 +16,6 @@ export const Component = { UserController: Symbol.for('UserController'), OfferController: Symbol.for('OfferController'), CommentController: Symbol.for('CommentController'), + AuthService: Symbol.for('AuthService'), + AuthExceptionFilter: Symbol.for('AuthExceptionFilter'), } as const; diff --git a/tsconfig.json b/tsconfig.json index dbd5167..20a2ce3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,11 +22,11 @@ "outDir": "./dist", "emitDecoratorMetadata": true, "types": [ - "node", + "node" ], "lib": [ "ESNext" - ] + ], }, "include": [ "src/**/*.ts" @@ -37,5 +37,6 @@ ], "ts-node": { "esm": true - } + }, + "files": ["./custom.d.ts"] }