Skip to content

Commit

Permalink
Merge pull request #11 from FoxMalder-coder/module8-task1
Browse files Browse the repository at this point in the history
  • Loading branch information
keksobot authored Nov 4, 2024
2 parents 2bfcb7b + d83d77e commit 549f5b3
Show file tree
Hide file tree
Showing 54 changed files with 494 additions and 101 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 { TokenPayload } from './src/shared/modules/auth/index.js';

declare module 'express-serve-static-core' {
export interface Request {
tokenPayload: TokenPayload;
}
}
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"got": "14.4.2",
"http-status-codes": "2.3.0",
"inversify": "6.0.2",
"jose": "5.9.6",
"mime-types": "2.1.35",
"mkdirp": "3.0.1",
"mongoose": "8.7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/import.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class ImportCommand implements Command {
price: offer.price,
facilities: offer.facilities,
type: offer.type,
city: offer.city,
cityName: offer.city.name,
isPremium: offer.isPremium,
rooms: offer.rooms,
guests: offer.guests,
Expand Down
4 changes: 3 additions & 1 deletion src/main.rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { Component } from './shared/const.js';
import { RESTApplication } from './rest/rest.application.js';
import { createUserContainer } from './shared/modules/user/index.js';
import { createOfferContainer } from './shared/modules/offer/index.js';
import { createCommentContainer } from './shared/modules/comments/index.js';
import { createCommentContainer } from './shared/modules/comment/index.js';
import { createAuthContainer } from './shared/modules/auth/index.js';

async function bootstrap() {
const container = Container.merge(
createRestApplicationContainer(),
createUserContainer(),
createOfferContainer(),
createCommentContainer(),
createAuthContainer(),
);

const application = container.get<RESTApplication>(Component.RestApplication);
Expand Down
5 changes: 5 additions & 0 deletions src/rest/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getMongoURI } from '../shared/helpers/database.js';
import express, { Express } from 'express';
import { ExceptionFilter } from '../shared/libs/rest/exception-filter/exception-filter.interface.js';
import { Controller } from '../shared/libs/rest/controller/controller.interface.js';
import { ParseTokenMiddleware } from '../shared/libs/rest/middleware/parse-token.middleware.js';

@injectable()
export class RESTApplication {
Expand All @@ -21,6 +22,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();
}
Expand Down Expand Up @@ -49,11 +51,14 @@ 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));
}

Expand Down
11 changes: 11 additions & 0 deletions src/shared/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,20 @@ 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;

export enum SortType {
Down = -1,
Up = 1,
}

export enum OfferType {
Apartment = 'apartment',
House = 'house',
Room = 'room',
Hotel = 'hotel',
}

export const FACILITIES = ['Breakfast', 'Air conditioning', 'Laptop friendly workspace', 'Baby seat', 'Washer', 'Towels', 'Fridge'];
7 changes: 7 additions & 0 deletions src/shared/libs/config/rest.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type RestSchema = {
DB_PORT: string;
DB_NAME: string;
UPLOAD_DIRECTORY: string;
JWT_SECRET: string;
}

export const configRestSchema = convict<RestSchema>({
Expand Down Expand Up @@ -63,4 +64,10 @@ export const configRestSchema = convict<RestSchema>({
env: 'UPLOAD_DIRECTORY',
default: null
},
JWT_SECRET: {
doc: 'Secret for sign JWT',
format: String,
env: 'JWT_SECRET',
default: null
},
});
4 changes: 2 additions & 2 deletions src/shared/libs/file-reader/tsv.file-reader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import EventEmitter from 'node:events';
import { createReadStream } from 'node:fs';
import { FileReader } from './file-reader.interface.js';
import { City, Location, Offer, OfferType, User } from '../../types/index.js';
import { CITIES, CITIES_LIST } from '../../const.js';
import { City, Location, Offer, User } from '../../types/index.js';
import { CITIES, CITIES_LIST, OfferType } from '../../const.js';

export class TSVFileReader extends EventEmitter implements FileReader {
private CHUNK_SIZE = 16384;
Expand Down
2 changes: 2 additions & 0 deletions src/shared/libs/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ export { Route } from './route.interface.js';
export { HttpError } from './http-error.js';
export { DocumentExistsMiddleware } from './middleware/document-exists.middleware.js';
export { UploadFileMiddleware } from './middleware/upload-file.middleware.js';
export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js';
export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js';
46 changes: 46 additions & 0 deletions src/shared/libs/rest/middleware/parse-token.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { createSecretKey } from 'node:crypto';
import { Request, Response, NextFunction } from 'express';
import { StatusCodes } from 'http-status-codes';
import { jwtVerify } from 'jose';
import { TokenPayload } from '../../../modules/auth/index.js';
import { HttpError } from '../http-error.js';
import { Middleware } from './middleware.interface.js';

function isTokenPayload(payload: unknown): payload is TokenPayload {
return (
(typeof payload === 'object' && payload !== null) &&
('email' in payload && typeof payload.email === '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<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',
'AuthenticateMiddleware')
);
}
}
}
18 changes: 18 additions & 0 deletions src/shared/libs/rest/middleware/private-route.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Request, Response, NextFunction } from 'express';
import { Middleware } from './middleware.interface.js';
import { HttpError } from '../index.js';
import { StatusCodes } from 'http-status-codes';

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

return next();
}
}
25 changes: 25 additions & 0 deletions src/shared/modules/auth/auth-exception-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from 'express';
import { injectable, inject } from 'inversify';
import { Component } from '../../const.js';
import { Logger } from '../../libs/logger/logger.interface.js';
import { ExceptionFilter } from '../../libs/rest/index.js';
import { BaseUserException } from './errors/base-user.exception.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 });
}
}
6 changes: 6 additions & 0 deletions src/shared/modules/auth/auth-service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { LoginUserDto, UserEntity } from '../user/index.js';

export interface AuthService {
authenticate(user: UserEntity): Promise<string>;
verify(dto: LoginUserDto): Promise<UserEntity>;
}
2 changes: 2 additions & 0 deletions src/shared/modules/auth/auth.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const JWT_ALGORITHM = 'HS256';
export const JWT_EXPIRED = '2d';
14 changes: 14 additions & 0 deletions src/shared/modules/auth/auth.container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Container } from 'inversify';
import { Component } from '../../const.js';
import { ExceptionFilter } from '../../libs/rest/index.js';
import { AuthExceptionFilter } from './auth-exception-filter.js';
import { AuthService } from './auth-service.interface.js';
import { DefaultAuthService } from './default-auth.service.js';

export function createAuthContainer() {
const authContainer = new Container();
authContainer.bind<AuthService>(Component.AuthService).to(DefaultAuthService).inSingletonScope();
authContainer.bind<ExceptionFilter>(Component.AuthExceptionFilter).to(AuthExceptionFilter).inSingletonScope();

return authContainer;
}
55 changes: 55 additions & 0 deletions src/shared/modules/auth/default-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { inject, injectable } from 'inversify';
import * as crypto from 'node:crypto';
import { Component } from '../../const.js';
import { Logger } from '../../libs/logger/logger.interface.js';
import { Config } from 'convict';
import { SignJWT } from 'jose';
import { RestSchema } from '../../libs/config/rest.schema.js';
import { LoginUserDto } from '../user/index.js';
import { UserService } from '../user/user-service.interface.js';
import { UserEntity } from '../user/user.entity.js';
import { AuthService } from './auth-service.interface.js';
import { JWT_ALGORITHM, JWT_EXPIRED } from './auth.constant.js';
import { TokenPayload } from './types/token-payload.type.js';
import { UserNotFoundException } from './errors/user-not-found.exception.js';
import { UserPasswordIncorrectException } from './errors/user-password-incorrect.exception.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<RestSchema>,
) { }

public async authenticate(user: UserEntity): Promise<string> {
const jwtSecret = this.config.get('JWT_SECRET');
const secretKey = crypto.createSecretKey(jwtSecret, 'utf-8');
const tokenPayload: TokenPayload = {
email: user.email,
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<UserEntity> {
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;
}
}
7 changes: 7 additions & 0 deletions src/shared/modules/auth/errors/base-user.exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { HttpError } from '../../../libs/rest/index.js';

export class BaseUserException extends HttpError {
constructor(httpStatusCode: number, message: string) {
super(httpStatusCode, message);
}
}
8 changes: 8 additions & 0 deletions src/shared/modules/auth/errors/user-not-found.exception.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
Original file line number Diff line number Diff line change
@@ -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');
}
}
7 changes: 7 additions & 0 deletions src/shared/modules/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { AuthService } from './auth-service.interface.js';
export { TokenPayload } from './types/token-payload.type.js';
export { DefaultAuthService } from './default-auth.service.js';
export { BaseUserException } from './errors/base-user.exception.js';
export { UserNotFoundException } from './errors/user-not-found.exception.js';
export { UserPasswordIncorrectException } from './errors/user-password-incorrect.exception.js';
export { createAuthContainer } from './auth.container.js';
4 changes: 4 additions & 0 deletions src/shared/modules/auth/types/token-payload.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type TokenPayload = {
email: string;
id: string;
};
Loading

0 comments on commit 549f5b3

Please sign in to comment.