Skip to content

Commit

Permalink
module8-task1: add auth logic
Browse files Browse the repository at this point in the history
  • Loading branch information
JIlyaS committed Nov 4, 2024
1 parent 975d272 commit f20afce
Show file tree
Hide file tree
Showing 51 changed files with 551 additions and 171 deletions.
1 change: 1 addition & 0 deletions .env-example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DB_PASSWORD=<Пароль для базы данных>
DB_PORT=<Порт для подключения к базе данных>
DB_NAME=<Имя базы данных>
UPLOAD_DIRECTORY=<Путь для хранения файлов>
JWT_SECRET=<Секрет для аутентификации>
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;
}
}
15 changes: 15 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 @@ -57,6 +57,7 @@
"express-async-handler": "1.2.0",
"got": "^14.2.1",
"inversify": "^6.0.2",
"jose": "5.4.0",
"mime-types": "2.1.35",
"mongoose": "8.3.4",
"multer": "1.4.5-lts.1",
Expand Down
2 changes: 2 additions & 0 deletions src/main.rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,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';
import { RestApplication } from './rest/index.js';
import { COMPONENT } from './shared/constants/index.js';

Expand All @@ -15,6 +16,7 @@ async function bootstrap() {
createUserContainer(),
createOfferContainer(),
createCommentContainer(),
createAuthContainer(),
);

const application = appContainer.get<RestApplication>(COMPONENT.REST_APPLICATION);
Expand Down
6 changes: 6 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 { 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 {
Expand All @@ -20,6 +21,7 @@ export class RestApplication {
@inject(COMPONENT.EXCEPTION_FILTER) private readonly appExceptionFilter: ExceptionFilter,
@inject(COMPONENT.USER_CONTROLLER) private readonly userController: Controller,
@inject(COMPONENT.COMMENT_CONTROLLER) private readonly commentController: Controller,
@inject(COMPONENT.AUTH_EXCEPTION_FILTER) private readonly authExceptionFilter: ExceptionFilter,
) {}

private async initDb() {
Expand All @@ -46,14 +48,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));
}

Expand Down
2 changes: 2 additions & 0 deletions src/shared/constants/component.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export const COMPONENT = {
EXCEPTION_FILTER: Symbol.for('kExceptionFilter'),
USER_CONTROLLER: Symbol.for('kUserController'),
COMMENT_CONTROLLER: Symbol.for('kCommentController'),
AUTH_SERVICE: Symbol.for('kAuthService'),
AUTH_EXCEPTION_FILTER: Symbol.for('kAuthExceptionFilter'),
} as const;
5 changes: 5 additions & 0 deletions src/shared/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public setPassword(password: string, salt: string) {
this.password = createSHA256(password, salt);
}

public verifyPassword(password: string, salt: string) {
const hashPassword = createSHA256(password, salt);
return hashPassword === this.password;
}

public getPassword() {
return this.password;
}
Expand Down
1 change: 1 addition & 0 deletions src/shared/libs/config/rest.schema.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface IRestSchema {
DB_PORT: string;
DB_NAME: string;
UPLOAD_DIRECTORY: string;
JWT_SECRET: string;
}
6 changes: 6 additions & 0 deletions src/shared/libs/config/rest.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,10 @@ export const configRestSchema = convict<IRestSchema>({
env: 'UPLOAD_DIRECTORY',
default: null
},
JWT_SECRET: {
doc: 'Secret for sign JWT',
format: String,
env: 'JWT_SECRET',
default: null
},
});
2 changes: 2 additions & 0 deletions src/shared/libs/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ export { ValidateObjectIdQueryMiddleware } from './middleware/validate-objectid-
export { DocumentQueryExistsMiddleware } from './middleware/document-query-exists.middleware.js';
export { DocumentBodyExistsMiddleware } from './middleware/document-body-exists.middleware.js';
export { ValidateObjectIdBodyMiddleware } from './middleware/validate-objectid-body.middleware.js';
export { ParseTokenMiddleware } from './middleware/parse-token.middleware.js';
export { PrivateRouteMiddleware } from './middleware/private-route.middleware.js';
49 changes: 49 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,49 @@
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') &&
('userName' in payload && typeof payload.userName === '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(String(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')
);
}
}
}
20 changes: 20 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,20 @@

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, _: Response, next: NextFunction): Promise<void> {
if (! tokenPayload) {
throw new HttpError(
StatusCodes.UNAUTHORIZED,
'Unauthorized',
'PrivateRouteMiddleware'
);
}

return next();
}
}
7 changes: 7 additions & 0 deletions src/shared/modules/auth/auth-service.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UserEntity } from '../../entities/index.js';
import { LoginUserDto } 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';
15 changes: 15 additions & 0 deletions src/shared/modules/auth/auth.container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Container } from 'inversify';

import { AuthService } from './auth-service.interface.js';
import { DefaultAuthService } from './default-auth.service.js';
import { ExceptionFilter } from '../../libs/rest/index.js';
import { AuthExceptionFilter } from './auth.exception-filter.js';
import { COMPONENT } from '../../constants/component.constant.js';

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

return authContainer;
}
29 changes: 29 additions & 0 deletions src/shared/modules/auth/auth.exception-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { inject, injectable } from 'inversify';
import { NextFunction, Request, Response } from 'express';

import { ExceptionFilter } from '../../libs/rest/index.js';
import { Logger } from '../../libs/logger/index.js';
import { BaseUserException } from './errors/index.js';
import { COMPONENT } from '../../constants/component.constant.js';

@injectable()
export class AuthExceptionFilter implements ExceptionFilter {
constructor(
@inject(COMPONENT.LOGGER) private readonly logger: Logger
) {
this.logger.info('Register AuthExceptionFilter');
}

public catch(error: unknown, _: 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,
});
}
}
56 changes: 56 additions & 0 deletions src/shared/modules/auth/default-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { inject, injectable } from 'inversify';
import * as crypto from 'node:crypto';
import { SignJWT } from 'jose';

import { AuthService } from './auth-service.interface.js';
import { Logger } from '../../libs/logger/index.js';
import { LoginUserDto, UserService } from '../user/index.js';
import { TokenPayload } from './types/tokenPayload.js';
import { Config, IRestSchema } from '../../libs/config/index.js';
import { UserNotFoundException, UserPasswordIncorrectException } from './errors/index.js';
import { JWT_ALGORITHM, JWT_EXPIRED } from './auth.constant.js';
import { COMPONENT } from '../../constants/component.constant.js';
import { UserEntity } from '../../entities/index.js';

@injectable()
export class DefaultAuthService implements AuthService {
constructor(
@inject(COMPONENT.LOGGER) private readonly logger: Logger,
@inject(COMPONENT.USER_SERVICE) private readonly userService: UserService,
@inject(COMPONENT.CONFIG) private readonly config: Config<IRestSchema>,
) {}

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

0 comments on commit f20afce

Please sign in to comment.