Skip to content

Commit

Permalink
OV-52: + merge
Browse files Browse the repository at this point in the history
  • Loading branch information
sergiy4 committed Aug 29, 2024
2 parents 9c5488e + 6e10b5b commit 1c21628
Show file tree
Hide file tree
Showing 28 changed files with 228 additions and 11 deletions.
7 changes: 7 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ DB_DIALECT=pg
DB_POOL_MIN=2
DB_POOL_MAX=10

#
# TOKEN
#
SECRET_KEY=IfcxMIXtiR3LC2CX0inOB3ozBuuTYqb7
EXPIRATION_TIME=24h

#
# AWS
#
Expand All @@ -30,3 +36,4 @@ OPEN_AI_KEY=SOME_SECRET_KEY
# SESSION
#
SESSION_KEY=SOME_SECRET_KEY

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@
"convict": "6.2.4",
"dotenv": "16.4.5",
"fastify": "4.28.1",
"fastify-plugin": "4.5.1",
"jose": "5.7.0",
"knex": "3.1.0",
"objection": "3.1.4",
"openai": "4.56.0",
Expand Down
12 changes: 9 additions & 3 deletions backend/src/bundles/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
type UserSignInResponseDto,
} from '~/bundles/users/users.js';
import { HttpCode, HttpError } from '~/common/http/http.js';
import { cryptService } from '~/common/services/services.js';
import { cryptService, tokenService } from '~/common/services/services.js';

import { UserValidationMessage } from './enums/enums.js';

Expand Down Expand Up @@ -46,7 +46,10 @@ class AuthService {
});
}

return user.toObject();
const userObject = user.toObject();
const { id } = userObject;
const token = await tokenService.createToken(id);
return { ...userObject, token };
}

public async signUp(
Expand All @@ -60,7 +63,10 @@ class AuthService {
status: HttpCode.BAD_REQUEST,
});
}
return this.userService.create(userRequestDto);
const user = await this.userService.create(userRequestDto);
const { id } = user;
const token = await tokenService.createToken(id);
return { ...user, token };
}
}

Expand Down
9 changes: 6 additions & 3 deletions backend/src/bundles/users/user.repository.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { UserEntity } from '~/bundles/users/user.entity.js';
import { type UserModel } from '~/bundles/users/user.model.js';
import { type Repository } from '~/common/types/types.js';

import { UserEntity } from '../../bundles/users/user.entity.js';

class UserRepository implements Repository {
private userModel: typeof UserModel;

public constructor(userModel: typeof UserModel) {
this.userModel = userModel;
}

public find(): ReturnType<Repository['find']> {
return Promise.resolve(null);
public async find(userId: string): Promise<UserEntity | null> {
const user = await this.userModel.query().findById(userId).execute();

return user ? UserEntity.initialize(user) : null;
}

public async findByEmail(email: string): Promise<UserEntity | null> {
Expand Down
6 changes: 3 additions & 3 deletions backend/src/bundles/users/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { UserEntity } from '~/bundles/users/user.entity.js';
import { type UserRepository } from '~/bundles/users/user.repository.js';
import { cryptService } from '~/common/services/services.js';
import { type Service } from '~/common/types/types.js';

import { UserEntity } from '../../bundles/users/user.entity.js';
import {
type UserGetAllResponseDto,
type UserSignUpRequestDto,
Expand All @@ -16,8 +16,8 @@ class UserService implements Service {
this.userRepository = userRepository;
}

public find(): ReturnType<Service['find']> {
return Promise.resolve(null);
public async find(id: string): Promise<UserEntity | null> {
return await this.userRepository.find(id);
}

public async findByEmail(email: string): Promise<UserEntity | null> {
Expand Down
1 change: 1 addition & 0 deletions backend/src/bundles/users/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export {
type UserSignUpRequestDto,
type UserSignUpResponseDto,
} from './types/types.js';
export { type UserEntity } from './user.entity.js';
export { UserModel } from './user.model.js';
export {
userSignInValidationSchema,
Expand Down
14 changes: 14 additions & 0 deletions backend/src/common/config/base-config.package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ class BaseConfig implements Config {
default: null,
},
},
TOKEN: {
SECRET_KEY: {
doc: 'Secret key for token generation',
format: String,
env: 'SECRET_KEY',
default: null,
},
EXPIRATION_TIME: {
doc: 'Token expiration time',
format: String,
env: 'EXPIRATION_TIME',
default: null,
},
},
AWS: {
ACCESS_KEY_ID: {
doc: 'AWS access key id',
Expand Down
4 changes: 4 additions & 0 deletions backend/src/common/config/types/environment-schema.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ type EnvironmentSchema = {
POOL_MIN: number;
POOL_MAX: number;
};
TOKEN: {
SECRET_KEY: string;
EXPIRATION_TIME: string;
};
AWS: {
ACCESS_KEY_ID: string;
SECRET_ACCESS_KEY: string;
Expand Down
1 change: 1 addition & 0 deletions backend/src/common/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { USER_PASSWORD_SALT_ROUNDS } from './user.constants.js';
export { WHITE_ROUTES } from './white-routes.constants.js';
14 changes: 14 additions & 0 deletions backend/src/common/constants/white-routes.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiPath, AuthApiPath } from 'shared';

const WHITE_ROUTES = [
{
path: `/api/v1${ApiPath.AUTH}${AuthApiPath.SIGN_IN}`,
method: 'POST',
},
{
path: `/api/v1${ApiPath.AUTH}${AuthApiPath.SIGN_UP}`,
method: 'POST',
},
];

export { WHITE_ROUTES };
58 changes: 58 additions & 0 deletions backend/src/common/plugins/auth/auth-jwt.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fp from 'fastify-plugin';
import { HttpCode, HttpError, HttpHeader } from 'shared';

import { userService } from '~/bundles/users/users.js';
import { tokenService } from '~/common/services/services.js';

import { ErrorMessage, Hook } from './enums/enums.js';
import { type Route } from './types/types.js';
import { isRouteInWhiteList } from './utils/utils.js';

type Options = {
routesWhiteList: Route[];
};

const authenticateJWT = fp<Options>((fastify, { routesWhiteList }, done) => {
fastify.decorateRequest('user', null);

fastify.addHook(Hook.PRE_HANDLER, async (request) => {
if (isRouteInWhiteList(routesWhiteList, request)) {
return;
}

const authHeader = request.headers[HttpHeader.AUTHORIZATION];

if (!authHeader) {
throw new HttpError({
message: ErrorMessage.MISSING_TOKEN,
status: HttpCode.UNAUTHORIZED,
});
}

const [, token] = authHeader.split(' ');

const userId = await tokenService.getUserIdFromToken(token as string);

if (!userId) {
throw new HttpError({
message: ErrorMessage.INVALID_TOKEN,
status: HttpCode.UNAUTHORIZED,
});
}

const user = await userService.find(userId);

if (!user) {
throw new HttpError({
message: ErrorMessage.MISSING_USER,
status: HttpCode.BAD_REQUEST,
});
}

request.user = user;
});

done();
});

export { authenticateJWT };
2 changes: 2 additions & 0 deletions backend/src/common/plugins/auth/enums/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { ErrorMessage } from './error-message.enum.js';
export { Hook } from './hook.enum.js';
7 changes: 7 additions & 0 deletions backend/src/common/plugins/auth/enums/error-message.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const ErrorMessage = {
MISSING_TOKEN: 'You are not logged in',
INVALID_TOKEN: 'Token is no longer valid. Please log in again.',
MISSING_USER: 'User with this id does not exist.',
} as const;

export { ErrorMessage };
5 changes: 5 additions & 0 deletions backend/src/common/plugins/auth/enums/hook.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const Hook = {
PRE_HANDLER: 'preHandler',
} as const;

export { Hook };
6 changes: 6 additions & 0 deletions backend/src/common/plugins/auth/types/route.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type Route = {
path: string;
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
};

export { type Route };
1 change: 1 addition & 0 deletions backend/src/common/plugins/auth/types/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { type Route } from './route.type.js';
15 changes: 15 additions & 0 deletions backend/src/common/plugins/auth/utils/check-white-routes.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type FastifyRequest } from 'fastify';

import { type Route } from '../types/types.js';

const isRouteInWhiteList = (
routesWhiteList: Route[],
request: FastifyRequest,
): boolean => {
return routesWhiteList.some(
(route) =>
route.path === request.url && route.method === request.method,
);
};

export { isRouteInWhiteList };
1 change: 1 addition & 0 deletions backend/src/common/plugins/auth/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { isRouteInWhiteList } from './check-white-routes.util.js';
1 change: 1 addition & 0 deletions backend/src/common/plugins/plugins.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { authenticateJWT } from './auth/auth-jwt.plugin.js';
export { session } from './session/session.plugin.js';
6 changes: 6 additions & 0 deletions backend/src/common/server-application/base-server-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
type ValidationSchema,
} from '~/common/types/types.js';

import { WHITE_ROUTES } from '../constants/constants.js';
import { authenticateJWT } from '../plugins/plugins.js';
import {
type ServerApp,
type ServerAppApi,
Expand Down Expand Up @@ -125,6 +127,10 @@ class BaseServerApp implements ServerApp {
}

private registerPlugins(): void {
this.app.register(authenticateJWT, {
routesWhiteList: WHITE_ROUTES,
});

this.app.register(session, {
services: {
config: this.config,
Expand Down
7 changes: 6 additions & 1 deletion backend/src/common/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { config } from '~/common/config/config.js';
import { CryptService } from './crypt/crypt.service.js';
import { FileService } from './file/file.service.js';
import { OpenAIService } from './open-ai/open-ai.service.js';
import { TokenService } from './token/token.services.js';

const openAIService = new OpenAIService(config);
const cryptService = new CryptService();
const fileService = new FileService(config);

export { cryptService, fileService, openAIService };
const secretKey = config.ENV.TOKEN.SECRET_KEY;
const expirationTime = config.ENV.TOKEN.EXPIRATION_TIME;
const tokenService = new TokenService(secretKey, expirationTime);

export { cryptService, fileService, openAIService, tokenService };
35 changes: 35 additions & 0 deletions backend/src/common/services/token/token.services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type JWTPayload as TokenPayload } from 'jose';
import { jwtVerify, SignJWT } from 'jose';

class TokenService {
private secretKey: Uint8Array;
private expirationTime: string;

public constructor(secretKey: string, expirationTime: string) {
this.secretKey = new TextEncoder().encode(secretKey);
this.expirationTime = expirationTime;
}

public async createToken(userId: string): Promise<string> {
return await new SignJWT({ userId })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime(this.expirationTime)
.sign(this.secretKey);
}

public async verifyToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.secretKey);
return payload;
} catch {
return null;
}
}

public async getUserIdFromToken(token: string): Promise<string | null> {
const payload = await this.verifyToken(token);
return (payload?.['userId'] as string) || null;
}
}

export { TokenService };
9 changes: 9 additions & 0 deletions backend/src/common/types/fastify.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'fastify';

import { type UserEntity } from '~/bundles/users/users.js';

declare module 'fastify' {
interface FastifyRequest {
user: UserEntity;
}
}
10 changes: 10 additions & 0 deletions package-lock.json

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

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
type UserSignInResponseDto = {
id: string;
email: string;
token?: string;
};

export { type UserSignInResponseDto };
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ type UserSignUpResponseDto = {
id: string;
fullName: string;
email: string;
token?: string;
};

export { type UserSignUpResponseDto };
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const userSignUp = z
.required()
.refine((data) => data.fullName.split(/\s+/).length >= 2, {
message: UserValidationMessage.FULL_NAME_INVALID,
path: ['name'],
path: ['fullName'],
})
.refine((data) => data.password === data.confirmPassword, {
message: UserValidationMessage.PASS_DONT_MATCH,
Expand Down
Loading

0 comments on commit 1c21628

Please sign in to comment.