Skip to content

Commit

Permalink
add token middlewares
Browse files Browse the repository at this point in the history
  • Loading branch information
rcole1919 committed Nov 1, 2024
1 parent ab6a2c9 commit ba10871
Show file tree
Hide file tree
Showing 21 changed files with 161 additions and 44 deletions.
7 changes: 7 additions & 0 deletions custom.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { TTokenPayload } from './src/shared/modules/auth/types/index.js';

declare module 'express-serve-static-core' {
export interface Request {
tokenPayload: TTokenPayload;
}
}
2 changes: 2 additions & 0 deletions src/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export { ValidateObjectIdMiddleware } from './middlewares/validate-objectid.midd
export { ValidateDTOMiddleware } from './middlewares/validate-dto.middleware.js';
export { DocumentExistsMiddleware } from './middlewares/document-exists.middleware.js';
export { UploadFileMiddleware } from './middlewares/upload-file.middleware.js';
export { ParseTokenMiddleware } from './middlewares/parse-token.middleware.js';
export { PrivateRouteMiddleware } from './middlewares/private-route.middleware.js';
40 changes: 40 additions & 0 deletions src/rest/middlewares/parse-token.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { NextFunction, Request, Response } from 'express';
import { jwtVerify } from 'jose';
import { StatusCodes } from 'http-status-codes';

import { createSecretKey } from 'node:crypto';

import { IMiddleware } from '../types/index.js';
import { HttpError } from '../index.js';
import { isTokenPayload } from '../../shared/helpers/index.js';

export class ParseTokenMiddleware implements IMiddleware {
constructor(private readonly jwtSecret: string) {}

public async execute(req: Request, _res: Response, next: NextFunction): Promise<void> {
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',
'ParseTokenMiddleware',
));
}
}
}
19 changes: 19 additions & 0 deletions src/rest/middlewares/private-route.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { StatusCodes } from 'http-status-codes';
import { NextFunction, Request, Response } from 'express';

import { IMiddleware } from '../types/index.js';
import { HttpError } from '../index.js';

export class PrivateRouteMiddleware implements IMiddleware {
public async execute({ tokenPayload }: Request, _res: Response, next: NextFunction): Promise<void> {
if (!tokenPayload) {
throw new HttpError(
StatusCodes.UNAUTHORIZED,
'Unauthorized',
'PrivateRouteMiddleware',
);
}

return next();
}
}
3 changes: 3 additions & 0 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { COMPONENT } from '../shared/constants/index.js';
import { IDatabaseClient } from '../shared/libs/database-client/types/index.js';
import { getMongoURI } from '../shared/helpers/index.js';
import { IController, IExceptionFilter } from './types/index.js';
import { ParseTokenMiddleware } from './index.js';

@injectable()
export class RestApplication {
Expand Down Expand Up @@ -47,11 +48,13 @@ export class RestApplication {
}

private 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 initExceptionFilters() {
Expand Down
2 changes: 1 addition & 1 deletion src/rest/types/middleware.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from 'express';

export interface IMiddleware {
execute(req: Request, res: Response, next: NextFunction): void;
execute(req: Request, res: Response, next: NextFunction): Promise<void> | void;
}
1 change: 1 addition & 0 deletions src/shared/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export {

export { getMongoURI } from './database.js';
export { createSHA256 } from './hash.js';
export { isTokenPayload } from './token.js';
8 changes: 8 additions & 0 deletions src/shared/helpers/token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { TTokenPayload } from '../modules/auth/types/index.js';

export const isTokenPayload = (payload: unknown): payload is TTokenPayload => ((
(typeof payload === 'object' && payload !== null) &&
('email' in payload && typeof payload.email === 'string') &&
('name' in payload && typeof payload.name === 'string') &&
('id' in payload && typeof payload.id === 'string')
));
8 changes: 6 additions & 2 deletions src/shared/modules/auth/default-auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { LoginUserDTO } from '../user/index.js';
import { IUserService } from '../user/types/index.js';
import { UserEntity } from '../user/user.entity.js';
import { IConfig, TRestSchema } from '../../libs/config/types/index.js';
import { UserNotFoundException, UserPasswordIncorrectException } from './errors/index.js';
import {
UserNotFoundException,
UserPasswordIncorrectException,
} from './errors/index.js';

@injectable()
export class DefualtAuthService implements IAuthService {
Expand All @@ -36,8 +39,9 @@ export class DefualtAuthService implements IAuthService {
.sign(secretKey);
}

public async verfy(dto: LoginUserDTO): Promise<UserEntity> {
public async verify(dto: LoginUserDTO): Promise<UserEntity> {
const user = await this.userService.findByEmail(dto.email);

if (!user) {
this.logger.warn(`User with ${dto.email} not found`);
throw new UserNotFoundException();
Expand Down
2 changes: 1 addition & 1 deletion src/shared/modules/auth/types/auth-service.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import { UserEntity } from '../../user/user.entity.js';

export interface IAuthService {
authenticate(user: UserEntity): Promise<string>;
verfy(dto: LoginUserDTO): Promise<UserEntity>;
verify(dto: LoginUserDTO): Promise<UserEntity>;
}
6 changes: 4 additions & 2 deletions src/shared/modules/comment/comment.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ValidateObjectIdMiddleware,
ValidateDTOMiddleware,
DocumentExistsMiddleware,
PrivateRouteMiddleware,
} from '../../../rest/index.js';
import { EHttpMethod } from '../../../rest/types/index.js';
import { COMPONENT } from '../../constants/index.js';
Expand Down Expand Up @@ -40,6 +41,7 @@ export class CommentController extends BaseController {
method: EHttpMethod.Post,
handler: this.create,
middlewares: [
new PrivateRouteMiddleware(),
new ValidateObjectIdMiddleware('offerId'),
new ValidateDTOMiddleware(CreateCommentDTO),
new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),
Expand All @@ -53,10 +55,10 @@ export class CommentController extends BaseController {
}

public async create(
{ params, body }: Request<TParamOfferId, unknown, CreateCommentDTO>,
{ params, body, tokenPayload }: Request<TParamOfferId, unknown, CreateCommentDTO>,
res: Response
): Promise<void> {
const result = await this.commentService.create(body, params.offerId);
const result = await this.commentService.create({ ...body, authorId: tokenPayload.id}, params.offerId);
this.created(res, fillDTO(CommentRDO, result));
}
}
2 changes: 1 addition & 1 deletion src/shared/modules/comment/comment.http
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Content-Type: application/json
## Создать комментарий
POST http://localhost:4000/comments/66f947e7e706754fb39b93ae HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6Im5ld0BvdmVybG9vay5uZXQiLCJuYW1lIjoiQXV0aCIsImlkIjoiNjcyNTMxMDdjNjE2NzZlYTczNGE4YjAzIiwiaWF0IjoxNzMwNDk1NjEyLCJleHAiOjE3MzA2Njg0MTJ9.KbhaENJBtEWavsUY4oLOjrYF0N4XhmJkOxXPJTWSjdU

{
"text": "asddfsdasc 23dfsdfsdf sdfs",
"authorId": "66f947e7e706754fb39b93a7",
"rating": 2
}

Expand Down
3 changes: 1 addition & 2 deletions src/shared/modules/comment/dto/create-comment.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IsMongoId, IsString, IsInt, Min, Max, Length } from 'class-validator';
import { IsString, IsInt, Min, Max, Length } from 'class-validator';

import { COMMENT_RATING, COMMENT_TEXT_LENGTH } from '../../../constants/index.js';

Expand All @@ -7,7 +7,6 @@ export class CreateCommentDTO {
@Length(COMMENT_TEXT_LENGTH.MIN, COMMENT_TEXT_LENGTH.MAX)
public text!: string;

@IsMongoId()
public authorId!: string;

@IsInt()
Expand Down
2 changes: 0 additions & 2 deletions src/shared/modules/offer/dto/create-offer.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
IsMongoId,
IsInt,
IsString,
IsEnum,
Expand Down Expand Up @@ -72,7 +71,6 @@ export class CreateOfferDTO {
@ArrayUnique<EFacilities>()
public facilities!: EFacilities[];

@IsMongoId()
public authorId!: string;

@ValidateNested()
Expand Down
12 changes: 9 additions & 3 deletions src/shared/modules/offer/offer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ValidateObjectIdMiddleware,
ValidateDTOMiddleware,
DocumentExistsMiddleware,
PrivateRouteMiddleware,
} from '../../../rest/index.js';
import { EHttpMethod } from '../../../rest/types/index.js';
import { COMPONENT, RADIX } from '../../constants/index.js';
Expand Down Expand Up @@ -37,7 +38,10 @@ export class OfferController extends BaseController {
path: '/',
method: EHttpMethod.Post,
handler: this.create,
middlewares: [new ValidateDTOMiddleware(CreateOfferDTO)]
middlewares: [
new PrivateRouteMiddleware(),
new ValidateDTOMiddleware(CreateOfferDTO),
]
});
this.addRoute({
path: '/premium',
Expand All @@ -59,6 +63,7 @@ export class OfferController extends BaseController {
method: EHttpMethod.Delete,
handler: this.delete,
middlewares: [
new PrivateRouteMiddleware(),
new ValidateObjectIdMiddleware('offerId'),
new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),
],
Expand All @@ -68,6 +73,7 @@ export class OfferController extends BaseController {
method: EHttpMethod.Patch,
handler: this.update,
middlewares: [
new PrivateRouteMiddleware(),
new ValidateObjectIdMiddleware('offerId'),
new ValidateDTOMiddleware(UpdateOfferDTO),
new DocumentExistsMiddleware(this.offerService, 'Offer', 'offerId'),
Expand All @@ -89,10 +95,10 @@ export class OfferController extends BaseController {
}

public async create(
{ body }: Request<Record<string, unknown>, Record<string, unknown>, CreateOfferDTO>,
{ body, tokenPayload }: Request<Record<string, unknown>, Record<string, unknown>, CreateOfferDTO>,
res: Response,
): Promise<void> {
const result = await this.offerService.create(body);
const result = await this.offerService.create({ ...body, authorId: tokenPayload.id });
this.created(res, fillDTO(FullOfferRDO, result));
}

Expand Down
8 changes: 1 addition & 7 deletions src/shared/modules/user/default-user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,7 @@ export class DefaultUserService implements IUserService {
}

public async findByEmail(email: string): Promise<DocumentType<UserEntity> | null> {
const result = await this.userModel
.aggregate([
{ $match: { email } },
])
.exec();

return result[0] || null;
return this.userModel.findOne({ email });
}

public async findOrCreate(dto: CreateUserDTO, salt: string): Promise<DocumentType<UserEntity>> {
Expand Down
1 change: 1 addition & 0 deletions src/shared/modules/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { CreateUserDTO } from './dto/create-user.dto.js';
export { LoginUserDTO } from './dto/login-user.dto.js';
export { UserRDO } from './rdo/user.rdo.js';
export { LoggedUserRDO } from './rdo/logged-user.rdo.js';
export { DefaultUserService } from './default-user.service.js';
export { createUserContainer } from './user.container.js';
export { populateFavorites } from './user.aggregation.js';
Expand Down
9 changes: 9 additions & 0 deletions src/shared/modules/user/rdo/logged-user.rdo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Expose } from 'class-transformer';

export class LoggedUserRDO {
@Expose()
public token!: string;

@Expose()
public email!: string;
}
46 changes: 30 additions & 16 deletions src/shared/modules/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { fillDTO } from '../../helpers/index.js';
import { CreateUserDTO, LoginUserDTO, UserRDO } from './index.js';
import { IOfferService, TParamOfferId } from '../offer/types/index.js';
import { ShortOfferRDO } from '../offer/index.js';
import { IAuthService } from '../auth/types/index.js';
import { LoggedUserRDO } from './index.js';

const MOCK_USER = '66f947e7e706754fb39b93a7';

Expand All @@ -29,6 +31,7 @@ export class UserController extends BaseController {
@inject(COMPONENT.USER_SERVICE) private readonly userService: IUserService,
@inject(COMPONENT.OFFER_SERVICE) private readonly offerService: IOfferService,
@inject(COMPONENT.CONFIG) private readonly config: IConfig<TRestSchema>,
@inject(COMPONENT.AUTH_SERVICE) private readonly authService: IAuthService,
) {
super(logger);

Expand All @@ -46,6 +49,11 @@ export class UserController extends BaseController {
handler: this.login,
middlewares: [new ValidateDTOMiddleware(LoginUserDTO)],
});
this.addRoute({
path: '/login',
method: EHttpMethod.Get,
handler: this.checkAuthenticate,
});
this.addRoute({ path: '/favorites/', method: EHttpMethod.Get, handler: this.showFavorites });
this.addRoute({
path: '/favorites/:offerId',
Expand Down Expand Up @@ -95,23 +103,15 @@ export class UserController extends BaseController {

public async login(
{ body }: TLoginUserRequest,
_res: Response,
res: Response,
): Promise<void> {
const existsUser = await this.userService.findByEmail(body.email);

if (!existsUser) {
throw new HttpError(
StatusCodes.BAD_REQUEST,
`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 showFavorites(_req: Request, res: Response): Promise<void> {
Expand Down Expand Up @@ -144,4 +144,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));
}
}
Loading

0 comments on commit ba10871

Please sign in to comment.