Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OV-5: Add jwt token #22

Merged
merged 80 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
5af58d6
OV-5: + add jose to installed packages
stefano-lacorazza Aug 19, 2024
5b37d18
OV-5: + add token-services.ts
stefano-lacorazza Aug 19, 2024
7cc363d
Merge branch 'task/OV-2-add-sign-in-flow' into task/OV-5-JWT-token
stefano-lacorazza Aug 19, 2024
e7d454f
OV-5: + add token to UserSignUpResponseDto
stefano-lacorazza Aug 20, 2024
0a5d350
OV-5: + return token in signin method
stefano-lacorazza Aug 20, 2024
e7d5c56
OV-5: * fix userId type in token from string to number
stefano-lacorazza Aug 20, 2024
9d4d9c5
OV-5: * move token services to appropiate folder
stefano-lacorazza Aug 20, 2024
1d0748a
OV-5: - remove trailing white space
stefano-lacorazza Aug 20, 2024
d1eab67
OV-5: * change to property shorthand
stefano-lacorazza Aug 20, 2024
d209ceb
OV-5: * refactor token-services in OOP
stefano-lacorazza Aug 20, 2024
f62acc2
OV-5: * use tokenService.createToken
stefano-lacorazza Aug 20, 2024
add55d7
OV-5: * add tokenservice to services and update imports
stefano-lacorazza Aug 20, 2024
68aba69
OV-5: * fix Wrong amount of left-padding spaces
stefano-lacorazza Aug 20, 2024
bf46eae
OV-5: * added final new line
stefano-lacorazza Aug 20, 2024
e112ea7
OV-5: * run prettier
stefano-lacorazza Aug 20, 2024
a7ce5be
OV-5: * use property shorthand
stefano-lacorazza Aug 20, 2024
8ce8243
OV-8: + empty auth jwt plugin
Sanchousina Aug 20, 2024
44b2532
OV-8: + register plugins
Sanchousina Aug 20, 2024
6154caf
Merge branch 'task/OV-2-add-sign-in-flow' into task/OV-5-JWT-token
stefano-lacorazza Aug 20, 2024
da1397e
OV-8: + dependencies
Sanchousina Aug 20, 2024
5af5016
OV-8: * merging with OV-5
Sanchousina Aug 20, 2024
31fae83
OV-5: * return user object and token
stefano-lacorazza Aug 20, 2024
3356aaf
OV-5: * added secretkey and expiration time to env
stefano-lacorazza Aug 20, 2024
618807b
OV-5: * move signup token creation to auth.service
stefano-lacorazza Aug 20, 2024
9022ef5
OV-5: + add getUserIdFromToken
stefano-lacorazza Aug 20, 2024
58bd5a3
OV-5: * remove Trailing whitespace
stefano-lacorazza Aug 20, 2024
9721153
OV-8: + findById method to Repository type
Sanchousina Aug 20, 2024
89ec60b
OV-8: + findById method to Service type
Sanchousina Aug 20, 2024
782a756
OV-5: * prettier
stefano-lacorazza Aug 20, 2024
b707dc3
OV-8: + findById method to user service and repository
Sanchousina Aug 20, 2024
5e46444
OV-8: * userId to number
Sanchousina Aug 20, 2024
50245d6
OV-8: + http codes to http code enum
Sanchousina Aug 20, 2024
ee3a4e5
OV-8: + hook and error messages enums for auth plugin
Sanchousina Aug 20, 2024
508cd31
Merge remote-tracking branch 'origin/task/OV-5-JWT-token' into task/O…
Sanchousina Aug 20, 2024
8eb1aae
OV-8: * export user entity
Sanchousina Aug 20, 2024
3561451
OV-8: * implement auth jwt plugin
Sanchousina Aug 20, 2024
f034695
OV-8: + white routes constant
Sanchousina Aug 20, 2024
36a220e
OV-8: * refactor checking for white route
Sanchousina Aug 20, 2024
9e4004b
OV-8: * use white routes constant
Sanchousina Aug 20, 2024
502aad9
OV-8: * extract fastify module augmentation into file
Sanchousina Aug 20, 2024
a9e0c53
Merge remote-tracking branch 'origin/next' into task/OV-8-protect-rou…
Sanchousina Aug 20, 2024
d4e33bc
OV-5: - remove jose from root dependencies
stefano-lacorazza Aug 21, 2024
8649f58
OV-5: * use config class for enviroment variables
stefano-lacorazza Aug 21, 2024
5c30474
OV-5: * use config class for enviroment variables
stefano-lacorazza Aug 21, 2024
34ea01d
Merge branch 'next' into task/OV-5-JWT-token
stefano-lacorazza Aug 21, 2024
f07ba3b
OV-5: * prettier
stefano-lacorazza Aug 21, 2024
218665a
OV-8: * modify find user method instead of adding findById
Sanchousina Aug 21, 2024
2f9f951
OV-8: * move dependencies to backend
Sanchousina Aug 21, 2024
9f17f91
OV-8: * white routes constant to include path method
Sanchousina Aug 21, 2024
3aaf539
OV-8: + route type
Sanchousina Aug 21, 2024
dbfec2d
OV-8: * checking if route is in white list with method
Sanchousina Aug 21, 2024
5a24c65
Merge remote-tracking branch 'origin/task/OV-5-JWT-token' into task/O…
Sanchousina Aug 21, 2024
b4fbbde
OV-8: + util function for checking route in white routes
Sanchousina Aug 21, 2024
a88eca1
OV-8: * modify find method in user repository instead of findById
Sanchousina Aug 21, 2024
1ac2392
OV-8: * use id instead of userId in types for repository and service
Sanchousina Aug 21, 2024
b4fe14c
Merge branch 'next' into task/OV-5-JWT-token
stefano-lacorazza Aug 22, 2024
9b53386
OV-5: * prettier
stefano-lacorazza Aug 22, 2024
0354932
OV-8: - remove commented code
Sanchousina Aug 23, 2024
4a7fc7e
OV-5: * change || to ??
stefano-lacorazza Aug 22, 2024
1723fa8
OV-5: - remove whitespace
stefano-lacorazza Aug 23, 2024
1ce3a2e
OV-5: * change getUserIdFromToken
stefano-lacorazza Aug 23, 2024
0a639aa
OV-5: * make token optional in UserSignInResponseDto
stefano-lacorazza Aug 25, 2024
e7c3c1c
OV-5: - delete user-sig-in.validation-schema.ts
stefano-lacorazza Aug 26, 2024
e9b8425
OV-8: * rename find to findById in service and repository
Sanchousina Aug 27, 2024
c1b02a8
Merge branch 'next' into task/OV-5-JWT-token
stefano-lacorazza Aug 27, 2024
70ec67d
OV-5: * fix base-config-package.ts and services.ts imports
stefano-lacorazza Aug 27, 2024
471c32b
OV-5: * prettier
stefano-lacorazza Aug 27, 2024
9e20e1c
Merge remote-tracking branch origin/task/OV-5-JWT-token into task/OV-…
Sanchousina Aug 27, 2024
f826db6
Merge pull request #33 from BinaryStudioAcademy/task/OV-8-protect-rou…
nikita-remeslov Aug 28, 2024
164b863
OV-5: * change createEnvSchema() to get envSchema()
stefano-lacorazza Aug 28, 2024
98edd98
Merge branch 'next' into task/OV-5-JWT-token
stefano-lacorazza Aug 28, 2024
537496e
OV-5: + add final newline to base configpackage
stefano-lacorazza Aug 28, 2024
0984b60
OV-5: * prettier
stefano-lacorazza Aug 28, 2024
ab05215
Merge branch 'next' into task/OV-5-JWT-token
stefano-lacorazza Aug 28, 2024
a8687aa
OV-5: * change id to string and findId to find
stefano-lacorazza Aug 28, 2024
c4bcd77
OV-5: * change notation
stefano-lacorazza Aug 29, 2024
b566e19
OV-5: * change notation to brackets
stefano-lacorazza Aug 29, 2024
113a204
OV-5: * change id notation
stefano-lacorazza Aug 29, 2024
56eae81
OV-5: * change class fields to camelCase
stefano-lacorazza Aug 29, 2024
d095953
OV-5: * UserId to id
stefano-lacorazza Aug 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -39,6 +39,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
11 changes: 8 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,9 @@ class AuthService {
});
}

return user.toObject();
const id = user.toObject().id;
const token = await tokenService.createToken(id);
return { ...user.toObject(), token };
stefano-lacorazza marked this conversation as resolved.
Show resolved Hide resolved
}

public async signUp(
Expand All @@ -60,7 +62,10 @@ class AuthService {
status: HttpCode.BAD_REQUEST,
});
}
return this.userService.create(userRequestDto);
const user = await this.userService.create(userRequestDto);
const id = user.id;
stefano-lacorazza marked this conversation as resolved.
Show resolved Hide resolved
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(userId: string): Promise<UserEntity | null> {
return await this.userRepository.find(userId);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to specify userId. If you are working inside of the users bundle it is understandable that find uses user id. REST API principles

}

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 @@ -74,6 +74,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 @@ -13,6 +13,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';
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
@@ -0,0 +1 @@
export { authenticateJWT } from './auth/auth-jwt.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 @@ -23,6 +23,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 @@ -124,6 +126,10 @@ class BaseServerApp implements ServerApp {
}

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

this.app.register(fastifyMultipart, {
limits: {
fileSize: Number.POSITIVE_INFINITY,
Expand Down
7 changes: 6 additions & 1 deletion backend/src/common/services/services.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { config } from '../config/config.js';
import { CryptService } from './crypt/crypt.service.js';
import { FileService } from './file/file.service.js';
import { TokenService } from './token/token.services.js';

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

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

export { cryptService, fileService, 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 SECRET_KEY: Uint8Array;
private EXPIRATION_TIME: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using camelCase for class fields throughout a project


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

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

public async verifyToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.SECRET_KEY);
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;
stefano-lacorazza marked this conversation as resolved.
Show resolved Hide resolved
};

export { type UserSignUpResponseDto };
2 changes: 2 additions & 0 deletions shared/src/framework/http/enums/http-code.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const HttpCode = {
NOT_FOUND: 404,
UNPROCESSED_ENTITY: 422,
INTERNAL_SERVER_ERROR: 500,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
} as const;

export { HttpCode };
Loading