diff --git a/backend/package.json b/backend/package.json index 9bba04e5f..03b5b292e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,13 +39,14 @@ "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", "pg": "8.12.0", "pino": "9.3.2", "pino-pretty": "10.3.1", "shared": "*", - "swagger-jsdoc": "6.2.8", - "jose": "5.7.0" + "swagger-jsdoc": "6.2.8" } } diff --git a/backend/src/bundles/users/user.repository.ts b/backend/src/bundles/users/user.repository.ts index f6b5873aa..8924dcf21 100644 --- a/backend/src/bundles/users/user.repository.ts +++ b/backend/src/bundles/users/user.repository.ts @@ -10,8 +10,10 @@ class UserRepository implements Repository { this.userModel = userModel; } - public find(): ReturnType { - return Promise.resolve(null); + public async findById(userId: number): Promise { + const user = await this.userModel.query().findById(userId).execute(); + + return user ? UserEntity.initialize(user) : null; } public async findByEmail(email: string): Promise { diff --git a/backend/src/bundles/users/user.service.ts b/backend/src/bundles/users/user.service.ts index c59215ed2..9109c5d03 100644 --- a/backend/src/bundles/users/user.service.ts +++ b/backend/src/bundles/users/user.service.ts @@ -16,8 +16,8 @@ class UserService implements Service { this.userRepository = userRepository; } - public find(): ReturnType { - return Promise.resolve(null); + public async findById(userId: number): Promise { + return await this.userRepository.findById(userId); } public async findByEmail(email: string): Promise { diff --git a/backend/src/bundles/users/users.ts b/backend/src/bundles/users/users.ts index 73f640e85..3c7531b73 100644 --- a/backend/src/bundles/users/users.ts +++ b/backend/src/bundles/users/users.ts @@ -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, diff --git a/backend/src/common/constants/constants.ts b/backend/src/common/constants/constants.ts index 172cdf50f..1a5d15784 100644 --- a/backend/src/common/constants/constants.ts +++ b/backend/src/common/constants/constants.ts @@ -1 +1,2 @@ export { USER_PASSWORD_SALT_ROUNDS } from './user.constants.js'; +export { WHITE_ROUTES } from './white-routes.constants.js'; diff --git a/backend/src/common/constants/white-routes.constants.ts b/backend/src/common/constants/white-routes.constants.ts new file mode 100644 index 000000000..faf1348e1 --- /dev/null +++ b/backend/src/common/constants/white-routes.constants.ts @@ -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 }; diff --git a/backend/src/common/plugins/auth/auth-jwt.plugin.ts b/backend/src/common/plugins/auth/auth-jwt.plugin.ts new file mode 100644 index 000000000..7eb20583c --- /dev/null +++ b/backend/src/common/plugins/auth/auth-jwt.plugin.ts @@ -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((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.findById(userId); + + if (!user) { + throw new HttpError({ + message: ErrorMessage.MISSING_USER, + status: HttpCode.BAD_REQUEST, + }); + } + + request.user = user; + }); + + done(); +}); + +export { authenticateJWT }; diff --git a/backend/src/common/plugins/auth/enums/enums.ts b/backend/src/common/plugins/auth/enums/enums.ts new file mode 100644 index 000000000..ab66478a4 --- /dev/null +++ b/backend/src/common/plugins/auth/enums/enums.ts @@ -0,0 +1,2 @@ +export { ErrorMessage } from './error-message.enum.js'; +export { Hook } from './hook.enum.js'; diff --git a/backend/src/common/plugins/auth/enums/error-message.enum.ts b/backend/src/common/plugins/auth/enums/error-message.enum.ts new file mode 100644 index 000000000..58e565b33 --- /dev/null +++ b/backend/src/common/plugins/auth/enums/error-message.enum.ts @@ -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 }; diff --git a/backend/src/common/plugins/auth/enums/hook.enum.ts b/backend/src/common/plugins/auth/enums/hook.enum.ts new file mode 100644 index 000000000..5588cd61c --- /dev/null +++ b/backend/src/common/plugins/auth/enums/hook.enum.ts @@ -0,0 +1,5 @@ +const Hook = { + PRE_HANDLER: 'preHandler', +} as const; + +export { Hook }; diff --git a/backend/src/common/plugins/auth/types/route.type.ts b/backend/src/common/plugins/auth/types/route.type.ts new file mode 100644 index 000000000..2ece81d01 --- /dev/null +++ b/backend/src/common/plugins/auth/types/route.type.ts @@ -0,0 +1,6 @@ +type Route = { + path: string; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; +}; + +export { type Route }; diff --git a/backend/src/common/plugins/auth/types/types.ts b/backend/src/common/plugins/auth/types/types.ts new file mode 100644 index 000000000..da236e3fd --- /dev/null +++ b/backend/src/common/plugins/auth/types/types.ts @@ -0,0 +1 @@ +export { type Route } from './route.type.js'; diff --git a/backend/src/common/plugins/auth/utils/check-white-routes.util.ts b/backend/src/common/plugins/auth/utils/check-white-routes.util.ts new file mode 100644 index 000000000..09f040f31 --- /dev/null +++ b/backend/src/common/plugins/auth/utils/check-white-routes.util.ts @@ -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 }; diff --git a/backend/src/common/plugins/auth/utils/utils.ts b/backend/src/common/plugins/auth/utils/utils.ts new file mode 100644 index 000000000..7dc2439bd --- /dev/null +++ b/backend/src/common/plugins/auth/utils/utils.ts @@ -0,0 +1 @@ +export { isRouteInWhiteList } from './check-white-routes.util.js'; diff --git a/backend/src/common/plugins/plugins.ts b/backend/src/common/plugins/plugins.ts new file mode 100644 index 000000000..edaf41ada --- /dev/null +++ b/backend/src/common/plugins/plugins.ts @@ -0,0 +1 @@ +export { authenticateJWT } from './auth/auth-jwt.plugin.js'; diff --git a/backend/src/common/server-application/base-server-app.ts b/backend/src/common/server-application/base-server-app.ts index 458e62b01..0a514c69c 100644 --- a/backend/src/common/server-application/base-server-app.ts +++ b/backend/src/common/server-application/base-server-app.ts @@ -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, @@ -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, diff --git a/backend/src/common/types/fastify.d.ts b/backend/src/common/types/fastify.d.ts new file mode 100644 index 000000000..f1b244180 --- /dev/null +++ b/backend/src/common/types/fastify.d.ts @@ -0,0 +1,9 @@ +import 'fastify'; + +import { type UserEntity } from '~/bundles/users/users.js'; + +declare module 'fastify' { + interface FastifyRequest { + user: UserEntity; + } +} diff --git a/backend/src/common/types/repository.type.ts b/backend/src/common/types/repository.type.ts index d69be849b..de7d12a83 100644 --- a/backend/src/common/types/repository.type.ts +++ b/backend/src/common/types/repository.type.ts @@ -1,5 +1,5 @@ type Repository = { - find(): Promise; + findById(id: number): Promise; findAll(): Promise; create(payload: unknown): Promise; update(): Promise; diff --git a/backend/src/common/types/service.type.ts b/backend/src/common/types/service.type.ts index 7b1a64ebb..a84006347 100644 --- a/backend/src/common/types/service.type.ts +++ b/backend/src/common/types/service.type.ts @@ -1,5 +1,5 @@ type Service = { - find(): Promise; + findById(id: number): Promise; findAll(): Promise<{ items: T[]; }>; diff --git a/package-lock.json b/package-lock.json index bbf8f8e55..9820704ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "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", diff --git a/shared/src/framework/http/enums/http-code.enum.ts b/shared/src/framework/http/enums/http-code.enum.ts index 6d2ad89ba..4f64e0c55 100644 --- a/shared/src/framework/http/enums/http-code.enum.ts +++ b/shared/src/framework/http/enums/http-code.enum.ts @@ -4,6 +4,8 @@ const HttpCode = { BAD_REQUEST: 400, UNPROCESSED_ENTITY: 422, INTERNAL_SERVER_ERROR: 500, + UNAUTHORIZED: 401, + FORBIDDEN: 403, } as const; export { HttpCode };