diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 18016b1a2..d479d55ff 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { - ".": "1.3.0", - "apps/backend": "0.0.0", - "apps/frontend": "0.0.0", - "packages/shared": "0.0.0" + ".": "1.9.0", + "apps/backend": "1.3.0", + "apps/frontend": "1.4.0", + "packages/shared": "1.4.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index c9294530e..ff9ff7057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## [1.9.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.8.0...v1.9.0) (2024-08-21) + + +### Features + +* Button component bb-26 ([#54](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/54)) ([e9d535d](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/e9d535d7685a93cf35eda81e196ee7af7c1e2d1d)) +* impl protected routing bb-11 ([#50](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/50)) ([5f2c71c](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/5f2c71ccfd1dd50f4a894d1ab391690b674ab2eb)) +* sign in screen - mobile responsiveness bb-61 ([#78](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/78)) ([f8b36f2](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/f8b36f25d71b15d0be03091bdae04b0f664b6ede)) + +## [1.8.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.7.0...v1.8.0) (2024-08-21) + + +### Features + +* add background component bb-24 ([#84](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/84)) ([55dd338](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/55dd338c10a3b34fa0dc959def4606b62571f0d5)) +* authorization token (JWT) bb-10 ([#40](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/40)) ([16e3c35](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/16e3c353ff700ab27a7b6af4fa7b3c17059cc916)) +* **backend:** add categories migration bb-34 ([#100](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/100)) ([43ececb](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/43ececb50a49723b2d0ff52ef1041dcfe49be225)) +* error handling bb-16 ([#57](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/57)) ([4425001](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/442500105802ba497c02978fd1e9af88eb6ae53d)) + +## [1.7.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.6.0...v1.7.0) (2024-08-21) + + +### Features + +* implement sign-in functionality bb-6 ([#37](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/37)) ([1f2b54c](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/1f2b54c94efa5e1553bc8b4bd24f2fdf2b0f8053)) + +## [1.6.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.5.0...v1.6.0) (2024-08-21) + + +### Features + +* Sign In bb-7 ([#49](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/49)) ([7132640](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/7132640bb557dfc3ab67c7ad131be828b576ef05)) + +## [1.5.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.4.0...v1.5.0) (2024-08-21) + + +### Features + +* added color variables to varibles.css bb-52 ([#93](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/93)) ([1544930](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/1544930f5ce7736753d6dd5af0c8383b12dcb1e9)) + +## [1.4.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.3.0...v1.4.0) (2024-08-20) + + +### Features + +* add Not Found page bb-51 ([#56](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/56)) ([befb626](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/befb626bfc67278a9aa40b3f460885807f35969b)) +* mobile linter rule adjust bb-2 ([#82](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/82)) ([584c838](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/584c8387a3150c317123beabb9c962de97f012f5)) + ## [1.3.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/v1.2.0...v1.3.0) (2024-08-20) diff --git a/apps/backend/.env.example b/apps/backend/.env.example index ea5285507..f1e9a4eb9 100644 --- a/apps/backend/.env.example +++ b/apps/backend/.env.example @@ -12,3 +12,10 @@ DB_CONNECTION_STRING=[db_client]://[db_username]:[db_user_password]@localhost:[d DB_DIALECT=pg DB_POOL_MIN=2 DB_POOL_MAX=10 + +# +# JWT +# +JWT_SECRET= +JWT_EXPIRATION_TIME=24hr +JWT_ALGORITHM=HS256 diff --git a/apps/backend/CHANGELOG.md b/apps/backend/CHANGELOG.md new file mode 100644 index 000000000..3af9f0ef9 --- /dev/null +++ b/apps/backend/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +## [1.3.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/backend-v1.2.0...backend-v1.3.0) (2024-08-21) + + +### Features + +* impl protected routing bb-11 ([#50](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/50)) ([5f2c71c](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/5f2c71ccfd1dd50f4a894d1ab391690b674ab2eb)) + +## [1.2.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/backend-v1.1.0...backend-v1.2.0) (2024-08-21) + + +### Features + +* authorization token (JWT) bb-10 ([#40](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/40)) ([16e3c35](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/16e3c353ff700ab27a7b6af4fa7b3c17059cc916)) +* **backend:** add categories migration bb-34 ([#100](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/100)) ([43ececb](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/43ececb50a49723b2d0ff52ef1041dcfe49be225)) + +## [1.1.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/backend-v1.0.0...backend-v1.1.0) (2024-08-21) + + +### Features + +* implement sign-in functionality bb-6 ([#37](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/37)) ([1f2b54c](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/1f2b54c94efa5e1553bc8b4bd24f2fdf2b0f8053)) + +## 1.0.0 (2024-08-20) + + +### Features + +* add continuous delivery bb-3 ([#62](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/62)) ([4106756](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/4106756bc7ecd9119ecf302b5d0df67089aa5961)) +* add mobile starter bb-2 ([#27](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/27)) ([a9381f3](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/a9381f3827dcce9b01c91deaa8343bc9fe212ffc)) +* add web starter bb-1 ([#4](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/4)) ([46357b5](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/46357b59ff3818e0e5bbdcb02b10a3689c9bb1d0)) +* sign up bb-8 ([#46](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/46)) ([833b096](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/833b096800fda00136885ae1bffedada943a1e8a)) diff --git a/apps/backend/package.json b/apps/backend/package.json index 81c9e2cf7..c55d5625a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -1,7 +1,7 @@ { "name": "backend", "type": "module", - "version": "1.0.0", + "version": "1.3.0", "engines": { "node": "20.x.x", "npm": "10.x.x" @@ -29,13 +29,15 @@ "convict": "6.2.4", "dotenv": "16.4.5", "fastify": "4.28.1", + "fastify-plugin": "4.5.1", + "jose": "5.6.3", "knex": "3.1.0", "objection": "3.1.4", "pg": "8.12.0", "pino": "9.3.2", "pino-pretty": "11.2.2", - "swagger-jsdoc": "6.2.8", - "shared": "*" + "shared": "*", + "swagger-jsdoc": "6.2.8" }, "devDependencies": { "@types/bcrypt": "5.0.2", diff --git a/apps/backend/src/db/migrations/20240821111749_create_categories_table.ts b/apps/backend/src/db/migrations/20240821111749_create_categories_table.ts new file mode 100644 index 000000000..94787a37b --- /dev/null +++ b/apps/backend/src/db/migrations/20240821111749_create_categories_table.ts @@ -0,0 +1,31 @@ +import { type Knex } from "knex"; + +const TABLE_NAME = "categories"; + +const ColumnName = { + CREATED_AT: "created_at", + ID: "id", + NAME: "name", + UPDATED_AT: "updated_at", +} as const; + +async function up(knex: Knex): Promise { + await knex.schema.createTable(TABLE_NAME, (table) => { + table.increments(ColumnName.ID).primary(); + table.string(ColumnName.NAME).notNullable().unique(); + table + .dateTime(ColumnName.CREATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + table + .dateTime(ColumnName.UPDATED_AT) + .notNullable() + .defaultTo(knex.fn.now()); + }); +} + +async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TABLE_NAME); +} + +export { down, up }; diff --git a/apps/backend/src/db/migrations/20240821131722_add_categories_data.ts b/apps/backend/src/db/migrations/20240821131722_add_categories_data.ts new file mode 100644 index 000000000..bf924a1a2 --- /dev/null +++ b/apps/backend/src/db/migrations/20240821131722_add_categories_data.ts @@ -0,0 +1,32 @@ +import { type Knex } from "knex"; + +const TABLE_NAME = "categories"; + +const ColumnName = { + NAME: "name", +} as const; + +const CATEGORIES = [ + "Physical", + "Work", + "Friends", + "Love", + "Money", + "Free time", + "Spiritual", + "Mental", +]; + +async function up(knex: Knex): Promise { + await knex(TABLE_NAME).insert( + CATEGORIES.map((name) => ({ + [ColumnName.NAME]: name, + })), + ); +} + +async function down(knex: Knex): Promise { + await knex(TABLE_NAME).whereIn(ColumnName.NAME, CATEGORIES).del(); +} + +export { down, up }; diff --git a/apps/backend/src/libs/enums/enums.ts b/apps/backend/src/libs/enums/enums.ts index 315515c08..496ed962f 100644 --- a/apps/backend/src/libs/enums/enums.ts +++ b/apps/backend/src/libs/enums/enums.ts @@ -1,2 +1,2 @@ export { RelationName } from "./relation-name.enum.js"; -export { APIPath, AppEnvironment, ServerErrorType } from "shared"; +export { APIPath, AppEnvironment, ErrorMessage, ServerErrorType } from "shared"; diff --git a/apps/backend/src/libs/modules/config/base-config.module.ts b/apps/backend/src/libs/modules/config/base-config.module.ts index 5b19b4058..ba47d40d6 100644 --- a/apps/backend/src/libs/modules/config/base-config.module.ts +++ b/apps/backend/src/libs/modules/config/base-config.module.ts @@ -76,6 +76,26 @@ class BaseConfig implements Config { format: Number, }, }, + JWT: { + ALGORITHM: { + default: null, + doc: "Token encryption algorithm", + env: "JWT_ALGORITHM", + format: String, + }, + EXPIRATION_TIME: { + default: null, + doc: "Token expiration time", + env: "JWT_EXPIRATION_TIME", + format: String, + }, + SECRET: { + default: null, + doc: "Used to sign and validate JWT tokens", + env: "JWT_SECRET", + format: String, + }, + }, }); } } diff --git a/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts b/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts index cb8abd649..7694d1325 100644 --- a/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts +++ b/apps/backend/src/libs/modules/config/libs/types/environment-schema.type.ts @@ -13,6 +13,11 @@ type EnvironmentSchema = { POOL_MAX: number; POOL_MIN: number; }; + JWT: { + ALGORITHM: string; + EXPIRATION_TIME: string; + SECRET: string; + }; }; export { type EnvironmentSchema }; diff --git a/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts b/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts index 58cf88e70..abdbed814 100644 --- a/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts +++ b/apps/backend/src/libs/modules/database/libs/enums/database-table-name.enum.ts @@ -1,4 +1,5 @@ const DatabaseTableName = { + CATEGORIES: "categories", MIGRATIONS: "migrations", USER_DETAILS: "user_details", USERS: "users", diff --git a/apps/backend/src/libs/modules/encrypt/base-encrypt.module.ts b/apps/backend/src/libs/modules/encrypt/base-encrypt.module.ts index 499ca10a4..725e5801d 100644 --- a/apps/backend/src/libs/modules/encrypt/base-encrypt.module.ts +++ b/apps/backend/src/libs/modules/encrypt/base-encrypt.module.ts @@ -1,6 +1,6 @@ import { genSalt, hash } from "bcrypt"; -import { Encrypt } from "./libs/types/types.js"; +import { type Encrypt } from "./libs/types/types.js"; class BaseEncrypt implements Encrypt { private saltRounds: number; @@ -17,9 +17,16 @@ class BaseEncrypt implements Encrypt { return await genSalt(this.saltRounds); } - public async encrypt( + public async compare( password: string, - ): Promise<{ hash: string; salt: string }> { + passwordHash: string, + salt: string, + ): Promise { + const hash = await this.generateHash(password, salt); + return hash === passwordHash; + } + + public async encrypt(password: string): ReturnType { const salt = await this.generateSalt(); const hash = await this.generateHash(password, salt); diff --git a/apps/backend/src/libs/modules/encrypt/libs/types/encrypt.type.ts b/apps/backend/src/libs/modules/encrypt/libs/types/encrypt.type.ts index 8d93bfd29..17d15cd2c 100644 --- a/apps/backend/src/libs/modules/encrypt/libs/types/encrypt.type.ts +++ b/apps/backend/src/libs/modules/encrypt/libs/types/encrypt.type.ts @@ -1,5 +1,11 @@ type Encrypt = { - encrypt: (data: string) => Promise<{ hash: string; salt: string }>; + compare: ( + password: string, + passwordHash: string, + salt: string, + ) => Promise; + + encrypt: (password: string) => Promise<{ hash: string; salt: string }>; }; export { type Encrypt }; diff --git a/apps/backend/src/libs/modules/http/http.ts b/apps/backend/src/libs/modules/http/http.ts index 7cdf1178a..55b0a1a04 100644 --- a/apps/backend/src/libs/modules/http/http.ts +++ b/apps/backend/src/libs/modules/http/http.ts @@ -1,3 +1,3 @@ -export { HTTPCode } from "./libs/enums/enums.js"; +export { HTTPCode, HTTPHeader } from "./libs/enums/enums.js"; export { HTTPError } from "./libs/exceptions/exceptions.js"; export { type HTTPMethod } from "./libs/types/types.js"; diff --git a/apps/backend/src/libs/modules/http/libs/enums/enums.ts b/apps/backend/src/libs/modules/http/libs/enums/enums.ts index 28f9dba93..dfc2a0cc1 100644 --- a/apps/backend/src/libs/modules/http/libs/enums/enums.ts +++ b/apps/backend/src/libs/modules/http/libs/enums/enums.ts @@ -1 +1 @@ -export { HTTPCode } from "shared"; +export { HTTPCode, HTTPHeader } from "shared"; diff --git a/apps/backend/src/libs/modules/server-application/base-server-application.ts b/apps/backend/src/libs/modules/server-application/base-server-application.ts index 60ee26e81..b45f1ec42 100644 --- a/apps/backend/src/libs/modules/server-application/base-server-application.ts +++ b/apps/backend/src/libs/modules/server-application/base-server-application.ts @@ -11,12 +11,16 @@ import { type Config } from "~/libs/modules/config/config.js"; import { type Database } from "~/libs/modules/database/database.js"; import { HTTPCode, HTTPError } from "~/libs/modules/http/http.js"; import { type Logger } from "~/libs/modules/logger/logger.js"; +import { token } from "~/libs/modules/token/token.js"; +import { authorizationPlugin } from "~/libs/plugins/plugins.js"; import { type ServerCommonErrorResponse, type ServerValidationErrorResponse, type ValidationSchema, } from "~/libs/types/types.js"; +import { userService } from "~/modules/users/users.js"; +import { WHITE_ROUTES } from "./libs/constants/constants.js"; import { type ServerApplication, type ServerApplicationApi, @@ -205,6 +209,12 @@ class BaseServerApplication implements ServerApplication { await this.app.register(swaggerUi, { routePrefix: `/${api.version}/documentation`, }); + + await this.app.register(authorizationPlugin, { + token, + userService, + whiteRoutes: WHITE_ROUTES, + }); }), ); } diff --git a/apps/backend/src/libs/modules/server-application/libs/constants/constants.ts b/apps/backend/src/libs/modules/server-application/libs/constants/constants.ts new file mode 100644 index 000000000..32b56c71d --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/constants/constants.ts @@ -0,0 +1,11 @@ +import { AuthApiPath } from "~/modules/auth/auth.js"; + +import { APIPath } from "../enums/enums.js"; + +const WHITE_ROUTES: string[] = [ + APIPath.AUTH, + `${APIPath.AUTH}${AuthApiPath.SIGN_IN}`, + `${APIPath.AUTH}${AuthApiPath.SIGN_UP}`, +]; + +export { WHITE_ROUTES }; diff --git a/apps/backend/src/libs/modules/server-application/libs/enums/enums.ts b/apps/backend/src/libs/modules/server-application/libs/enums/enums.ts new file mode 100644 index 000000000..a58bf9395 --- /dev/null +++ b/apps/backend/src/libs/modules/server-application/libs/enums/enums.ts @@ -0,0 +1 @@ +export { APIPath } from "~/libs/enums/enums.js"; diff --git a/apps/backend/src/libs/modules/token/base-token.module.ts b/apps/backend/src/libs/modules/token/base-token.module.ts new file mode 100644 index 000000000..39b294089 --- /dev/null +++ b/apps/backend/src/libs/modules/token/base-token.module.ts @@ -0,0 +1,33 @@ +import { JWTPayload, jwtVerify, JWTVerifyResult, SignJWT } from "jose"; + +type Constructor = { + algorithm: string; + expirationTime: string; + secret: Uint8Array; +}; + +class BaseToken { + private algorithm: string; + private expirationTime: string; + private secret: Uint8Array; + + constructor({ algorithm, expirationTime, secret }: Constructor) { + this.secret = secret; + this.algorithm = algorithm; + this.expirationTime = expirationTime; + } + + public async createToken(payload: T): Promise { + return await new SignJWT(payload) + .setProtectedHeader({ alg: this.algorithm }) + .setIssuedAt() + .setExpirationTime(this.expirationTime) + .sign(this.secret); + } + + public async decode(token: string): Promise> { + return await jwtVerify(token, this.secret); + } +} + +export { BaseToken }; diff --git a/apps/backend/src/libs/modules/token/token.ts b/apps/backend/src/libs/modules/token/token.ts new file mode 100644 index 000000000..951ee2ac9 --- /dev/null +++ b/apps/backend/src/libs/modules/token/token.ts @@ -0,0 +1,13 @@ +import { config } from "~/libs/modules/config/config.js"; +import { TokenPayload } from "~/libs/types/types.js"; + +import { BaseToken } from "./base-token.module.js"; + +const token = new BaseToken({ + algorithm: config.ENV.JWT.ALGORITHM, + expirationTime: config.ENV.JWT.EXPIRATION_TIME, + secret: Buffer.from(config.ENV.JWT.SECRET), +}); + +export { BaseToken } from "./base-token.module.js"; +export { token }; diff --git a/apps/backend/src/libs/plugins/authorization-plugin/authorization-plugin.ts b/apps/backend/src/libs/plugins/authorization-plugin/authorization-plugin.ts new file mode 100644 index 000000000..7067c9e57 --- /dev/null +++ b/apps/backend/src/libs/plugins/authorization-plugin/authorization-plugin.ts @@ -0,0 +1,58 @@ +import fp from "fastify-plugin"; + +import { ErrorMessage } from "~/libs/enums/enums.js"; +import { HTTPHeader } from "~/libs/modules/http/http.js"; +import { BaseToken } from "~/libs/modules/token/token.js"; +import { TokenPayload } from "~/libs/types/types.js"; +import { AuthError } from "~/modules/auth/auth.js"; +import { UserService } from "~/modules/users/users.js"; + +import { ServerHooks } from "../libs/enums/enums.js"; + +type PluginOptions = { + token: BaseToken; + userService: UserService; + whiteRoutes?: string[]; +}; + +const authorizationPlugin = fp( + (app, { token, userService, whiteRoutes = [] }) => { + app.addHook(ServerHooks.PRE_HANDLER, async (request) => { + const whiteRoute = whiteRoutes.find( + (route) => request.routeOptions.url === route, + ); + + if (whiteRoute) { + return; + } + + const header = request.headers[HTTPHeader.AUTHORIZATION]; + + if (!header) { + throw new AuthError({ message: ErrorMessage.UNAUTHORIZED }); + } + + try { + const { + payload: { userId }, + } = await token.decode(header); + + const user = await userService.find(userId); + + if (!user) { + throw new AuthError({ message: ErrorMessage.UNAUTHORIZED }); + } + + request.user = user.toObject(); + } catch (error) { + if (error instanceof AuthError) { + throw error; + } + + throw new AuthError({ message: ErrorMessage.UNAUTHORIZED }); + } + }); + }, +); + +export { authorizationPlugin }; diff --git a/apps/backend/src/libs/plugins/authorization-plugin/libs/types/authorization-plugin.d.ts b/apps/backend/src/libs/plugins/authorization-plugin/libs/types/authorization-plugin.d.ts new file mode 100644 index 000000000..d432f7f6c --- /dev/null +++ b/apps/backend/src/libs/plugins/authorization-plugin/libs/types/authorization-plugin.d.ts @@ -0,0 +1,9 @@ +import "fastify"; + +import { UserDto } from "~/libs/types/types.js"; + +declare module "fastify" { + interface FastifyRequest { + user?: UserDto; + } +} diff --git a/apps/backend/src/libs/plugins/libs/enums/enums.ts b/apps/backend/src/libs/plugins/libs/enums/enums.ts new file mode 100644 index 000000000..5e55c3757 --- /dev/null +++ b/apps/backend/src/libs/plugins/libs/enums/enums.ts @@ -0,0 +1 @@ +export { ServerHooks } from "./server-hooks.enum.js"; diff --git a/apps/backend/src/libs/plugins/libs/enums/server-hooks.enum.ts b/apps/backend/src/libs/plugins/libs/enums/server-hooks.enum.ts new file mode 100644 index 000000000..e40f40723 --- /dev/null +++ b/apps/backend/src/libs/plugins/libs/enums/server-hooks.enum.ts @@ -0,0 +1,5 @@ +const ServerHooks = { + PRE_HANDLER: "preHandler", +} as const; + +export { ServerHooks }; diff --git a/apps/backend/src/libs/plugins/plugins.ts b/apps/backend/src/libs/plugins/plugins.ts new file mode 100644 index 000000000..96ea2e327 --- /dev/null +++ b/apps/backend/src/libs/plugins/plugins.ts @@ -0,0 +1 @@ +export { authorizationPlugin } from "./authorization-plugin/authorization-plugin.js"; diff --git a/apps/backend/src/libs/types/repository.type.ts b/apps/backend/src/libs/types/repository.type.ts index 0236abdbe..b16860fe6 100644 --- a/apps/backend/src/libs/types/repository.type.ts +++ b/apps/backend/src/libs/types/repository.type.ts @@ -1,7 +1,7 @@ type Repository = { create(payload: unknown): Promise; delete(): Promise; - find(): Promise; + find(id: number): Promise; findAll(): Promise; update(): Promise; }; diff --git a/apps/backend/src/libs/types/service.type.ts b/apps/backend/src/libs/types/service.type.ts index e9c6bd66d..bacb0103a 100644 --- a/apps/backend/src/libs/types/service.type.ts +++ b/apps/backend/src/libs/types/service.type.ts @@ -1,7 +1,7 @@ type Service = { create(payload: unknown): Promise; delete(): Promise; - find(): Promise; + find(id: number): Promise; findAll(): Promise<{ items: T[]; }>; diff --git a/apps/backend/src/libs/types/types.ts b/apps/backend/src/libs/types/types.ts index d3a7648f9..9d5915629 100644 --- a/apps/backend/src/libs/types/types.ts +++ b/apps/backend/src/libs/types/types.ts @@ -4,6 +4,8 @@ export { type Service } from "./service.type.js"; export { type ServerCommonErrorResponse, type ServerValidationErrorResponse, + type TokenPayload, + type UserDto, type ValidationSchema, type ValueOf, } from "shared"; diff --git a/apps/backend/src/modules/auth/auth.controller.ts b/apps/backend/src/modules/auth/auth.controller.ts index ef81d5815..f9e6ee458 100644 --- a/apps/backend/src/modules/auth/auth.controller.ts +++ b/apps/backend/src/modules/auth/auth.controller.ts @@ -7,6 +7,8 @@ import { import { HTTPCode } from "~/libs/modules/http/http.js"; import { type Logger } from "~/libs/modules/logger/logger.js"; import { + type UserSignInRequestDto, + userSignInValidationSchema, type UserSignUpRequestDto, userSignUpValidationSchema, } from "~/modules/users/users.js"; @@ -35,6 +37,64 @@ class AuthController extends BaseController { body: userSignUpValidationSchema, }, }); + + this.addRoute({ + handler: (options) => + this.signIn( + options as APIHandlerOptions<{ + body: UserSignInRequestDto; + }>, + ), + method: "POST", + path: AuthApiPath.SIGN_IN, + validation: { + body: userSignInValidationSchema, + }, + }); + } + + /** + * @swagger + * /auth/sign-in: + * post: + * description: Sign in user into the system + * requestBody: + * description: User auth data + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * email: + * type: string + * format: email + * password: + * type: string + * responses: + * 200: + * description: Successful operation + * content: + * application/json: + * schema: + * type: object + * properties: + * user: + * $ref: "#/components/schemas/User" + * token: + * type: string + * description: "Authentication token for the user." + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..." + */ + private async signIn( + options: APIHandlerOptions<{ + body: UserSignInRequestDto; + }>, + ): Promise { + return { + payload: await this.authService.signIn(options.body), + status: HTTPCode.OK, + }; } /** @@ -63,10 +123,14 @@ class AuthController extends BaseController { * schema: * type: object * properties: - * message: - * type: object + * user: * $ref: "#/components/schemas/User" + * token: + * type: string + * description: "Authentication token for the user." + * example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ..." */ + private async signUp( options: APIHandlerOptions<{ body: UserSignUpRequestDto; diff --git a/apps/backend/src/modules/auth/auth.service.ts b/apps/backend/src/modules/auth/auth.service.ts index a02a7acea..d0296c65f 100644 --- a/apps/backend/src/modules/auth/auth.service.ts +++ b/apps/backend/src/modules/auth/auth.service.ts @@ -1,4 +1,9 @@ +import { ErrorMessage } from "~/libs/enums/enums.js"; +import { Encrypt } from "~/libs/modules/encrypt/encrypt.js"; +import { token } from "~/libs/modules/token/token.js"; import { + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from "~/modules/users/libs/types/types.js"; @@ -8,10 +13,47 @@ import { HTTPCode, UserValidationMessage } from "./libs/enums/enums.js"; import { AuthError } from "./libs/exceptions/exceptions.js"; class AuthService { + private encrypt: Encrypt; private userService: UserService; - public constructor(userService: UserService) { + public constructor(userService: UserService, encrypt: Encrypt) { this.userService = userService; + this.encrypt = encrypt; + } + + public async signIn( + userRequestDto: UserSignInRequestDto, + ): Promise { + const { email, password } = userRequestDto; + + const user = await this.userService.findByEmail(email); + + if (!user) { + throw new AuthError({ + message: ErrorMessage.INCORRECT_CREDENTIALS, + status: HTTPCode.UNAUTHORIZED, + }); + } + + const userDetails = user.toObject(); + + const jwtToken = await token.createToken({ userId: userDetails.id }); + + const { passwordHash, passwordSalt } = user.toNewObject(); + const isPasswordValid = await this.encrypt.compare( + password, + passwordHash, + passwordSalt, + ); + + if (!isPasswordValid) { + throw new AuthError({ + message: ErrorMessage.INCORRECT_CREDENTIALS, + status: HTTPCode.UNAUTHORIZED, + }); + } + + return { token: jwtToken, user: userDetails }; } public async signUp( @@ -27,7 +69,11 @@ class AuthService { }); } - return await this.userService.create(userRequestDto); + const user = await this.userService.create(userRequestDto); + + const jwtToken = await token.createToken({ userId: user.id }); + + return { token: jwtToken, user }; } } diff --git a/apps/backend/src/modules/auth/auth.ts b/apps/backend/src/modules/auth/auth.ts index 3673c72cb..562094e30 100644 --- a/apps/backend/src/modules/auth/auth.ts +++ b/apps/backend/src/modules/auth/auth.ts @@ -1,10 +1,13 @@ +import { encrypt } from "~/libs/modules/encrypt/encrypt.js"; import { logger } from "~/libs/modules/logger/logger.js"; import { userService } from "~/modules/users/users.js"; import { AuthController } from "./auth.controller.js"; import { AuthService } from "./auth.service.js"; -const authService = new AuthService(userService); +const authService = new AuthService(userService, encrypt); const authController = new AuthController(logger, authService); export { authController }; +export { AuthApiPath } from "./libs/enums/enums.js"; +export { AuthError } from "./libs/exceptions/exceptions.js"; diff --git a/apps/backend/src/modules/users/libs/types/types.ts b/apps/backend/src/modules/users/libs/types/types.ts index 59b9ffcef..6acbad458 100644 --- a/apps/backend/src/modules/users/libs/types/types.ts +++ b/apps/backend/src/modules/users/libs/types/types.ts @@ -1,6 +1,8 @@ export { type UserDto, type UserGetAllResponseDto, + type UserSignInRequestDto, + type UserSignInResponseDto, type UserSignUpRequestDto, type UserSignUpResponseDto, } from "shared"; diff --git a/apps/backend/src/modules/users/libs/validation-schemas/validation-schemas.ts b/apps/backend/src/modules/users/libs/validation-schemas/validation-schemas.ts index b17c12fbf..edf30c47b 100644 --- a/apps/backend/src/modules/users/libs/validation-schemas/validation-schemas.ts +++ b/apps/backend/src/modules/users/libs/validation-schemas/validation-schemas.ts @@ -1 +1 @@ -export { userSignUpValidationSchema } from "shared"; +export { userSignInValidationSchema, userSignUpValidationSchema } from "shared"; diff --git a/apps/backend/src/modules/users/user.repository.ts b/apps/backend/src/modules/users/user.repository.ts index e4749ceaa..d466adc9b 100644 --- a/apps/backend/src/modules/users/user.repository.ts +++ b/apps/backend/src/modules/users/user.repository.ts @@ -53,8 +53,20 @@ class UserRepository implements Repository { return Promise.resolve(true); } - public find(): ReturnType { - return Promise.resolve(null); + public async find(id: number): Promise { + const user = await this.userModel.query().findById(id); + + return user + ? UserEntity.initialize({ + createdAt: user.createdAt, + email: user.email, + id: user.id, + name: user.userDetails.name, + passwordHash: user.passwordHash, + passwordSalt: user.passwordSalt, + updatedAt: user.updatedAt, + }) + : null; } public async findAll(): Promise { diff --git a/apps/backend/src/modules/users/user.service.ts b/apps/backend/src/modules/users/user.service.ts index 6d5e79826..2f0376674 100644 --- a/apps/backend/src/modules/users/user.service.ts +++ b/apps/backend/src/modules/users/user.service.ts @@ -7,7 +7,6 @@ import { type UserDto, type UserGetAllResponseDto, type UserSignUpRequestDto, - type UserSignUpResponseDto, } from "./libs/types/types.js"; class UserService implements Service { @@ -19,12 +18,10 @@ class UserService implements Service { this.encrypt = encrypt; } - public async create( - payload: UserSignUpRequestDto, - ): Promise { + public async create(payload: UserSignUpRequestDto): Promise { const { hash, salt } = await this.encrypt.encrypt(payload.password); - const item = await this.userRepository.create( + const user = await this.userRepository.create( UserEntity.initializeNew({ email: payload.email, name: payload.name, @@ -33,15 +30,15 @@ class UserService implements Service { }), ); - return item.toObject(); + return user.toObject(); } public delete(): ReturnType { return Promise.resolve(true); } - public find(): ReturnType { - return Promise.resolve(null); + public async find(id: number): Promise { + return await this.userRepository.find(id); } public async findAll(): Promise { @@ -52,9 +49,8 @@ class UserService implements Service { }; } - public async findByEmail(email: string): Promise { - const item = await this.userRepository.findByEmail(email); - return item ? item.toObject() : null; + public findByEmail(email: string): Promise { + return this.userRepository.findByEmail(email); } public update(): ReturnType { diff --git a/apps/backend/src/modules/users/users.ts b/apps/backend/src/modules/users/users.ts index 77aaf3700..6a069a7b9 100644 --- a/apps/backend/src/modules/users/users.ts +++ b/apps/backend/src/modules/users/users.ts @@ -11,7 +11,16 @@ const userRepository = new UserRepository(UserModel, UserDetailsModel); const userService = new UserService(userRepository, encrypt); const userController = new UserController(logger, userService); -export { userController, userService }; export { UserValidationMessage } from "./libs/enums/enums.js"; -export { type UserSignUpRequestDto } from "./libs/types/types.js"; -export { userSignUpValidationSchema } from "./libs/validation-schemas/validation-schemas.js"; +export { + type UserSignInRequestDto, + type UserSignInResponseDto, + type UserSignUpRequestDto, + type UserSignUpResponseDto, +} from "./libs/types/types.js"; +export { + userSignInValidationSchema, + userSignUpValidationSchema, +} from "./libs/validation-schemas/validation-schemas.js"; +export { UserService } from "./user.service.js"; +export { userController, userService }; diff --git a/apps/frontend/CHANGELOG.md b/apps/frontend/CHANGELOG.md new file mode 100644 index 000000000..2214a0e3f --- /dev/null +++ b/apps/frontend/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +## [1.4.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/frontend-v1.3.0...frontend-v1.4.0) (2024-08-21) + + +### Features + +* sign in screen - mobile responsiveness bb-61 ([#78](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/78)) ([f8b36f2](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/f8b36f25d71b15d0be03091bdae04b0f664b6ede)) + +## [1.3.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/frontend-v1.2.0...frontend-v1.3.0) (2024-08-21) + + +### Features + +* authorization token (JWT) bb-10 ([#40](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/40)) ([16e3c35](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/16e3c353ff700ab27a7b6af4fa7b3c17059cc916)) +* error handling bb-16 ([#57](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/57)) ([4425001](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/442500105802ba497c02978fd1e9af88eb6ae53d)) + +## [1.2.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/frontend-v1.1.0...frontend-v1.2.0) (2024-08-21) + + +### Features + +* Sign In bb-7 ([#49](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/49)) ([7132640](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/7132640bb557dfc3ab67c7ad131be828b576ef05)) + +## [1.1.0](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/compare/frontend-v1.0.0...frontend-v1.1.0) (2024-08-21) + + +### Features + +* added color variables to varibles.css bb-52 ([#93](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/93)) ([1544930](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/1544930f5ce7736753d6dd5af0c8383b12dcb1e9)) + +## 1.0.0 (2024-08-20) + + +### Features + +* add loader bb-15 ([#45](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/45)) ([52a0f73](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/52a0f734f6e85125b4db6129afdafe5e9fae57ba)) +* add mobile starter bb-2 ([#27](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/27)) ([a9381f3](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/a9381f3827dcce9b01c91deaa8343bc9fe212ffc)) +* add Not Found page bb-51 ([#56](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/56)) ([befb626](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/befb626bfc67278a9aa40b3f460885807f35969b)) +* add web starter bb-1 ([#4](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/4)) ([46357b5](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/46357b59ff3818e0e5bbdcb02b10a3689c9bb1d0)) +* get authenticated user bb-14 ([#53](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/53)) ([0b147e2](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/0b147e2ca611f5859c8e81917e5a39ed8ad1971a)) +* protected routing bb-12 ([#39](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/39)) ([641f701](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/641f70179bb200f12c6a5833557a0cb494a39979)) +* sign up bb-8 ([#46](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/issues/46)) ([833b096](https://github.com/BinaryStudioAcademy/bsa-2024-bebalance/commit/833b096800fda00136885ae1bffedada943a1e8a)) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index bea485cea..8fcb309ba 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "1.4.0", "type": "module", "engines": { "node": "20.x.x", @@ -25,6 +25,7 @@ "react-hook-form": "7.52.2", "react-redux": "9.1.2", "react-router-dom": "6.26.1", + "react-toastify": "10.0.5", "shared": "*" }, "devDependencies": { diff --git a/apps/frontend/src/assets/css/variables.css b/apps/frontend/src/assets/css/variables.css index 1b666bfa4..d163b9f4c 100644 --- a/apps/frontend/src/assets/css/variables.css +++ b/apps/frontend/src/assets/css/variables.css @@ -1,8 +1,29 @@ :root { --background: #76acec; + --background-gray: #f6f5fa; + --black: #000000; + --brand: #0085ff; + --error: #df1c3d; --light-blue: #b4d6ff; --light-brand: #00f0ff; - --brand: #0085ff; + --light-gray: #e0e0e0; --white: #ffffff; - --black: #000000; + --complete: #c2eacd; + --completed: #f0fff4; + --physical-start: #ffe307; + --physical-end: #fff8b5; + --work-start: #69ff35; + --work-end: #c3ff19; + --friend-start: #7f21ce; + --friend-end: #cb00ff; + --love-start: #ff4040; + --love-end: #ff9432; + --money-start: #20b82f; + --money-end: #00f4d6; + --free-time-start: #ff5794; + --free-time-end: #fc72ff; + --spiritual-start: #ff7a00; + --spiritual-end: #ffe200; + --mental-start: #0085ff; + --mental-end: #00f0ff; } diff --git a/apps/frontend/src/assets/img/ripple-effect-bg.svg b/apps/frontend/src/assets/img/ripple-effect-bg.svg new file mode 100644 index 000000000..c8b7fed84 --- /dev/null +++ b/apps/frontend/src/assets/img/ripple-effect-bg.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/assets/img/ripple-effect-bg2.svg b/apps/frontend/src/assets/img/ripple-effect-bg2.svg new file mode 100644 index 000000000..0b382eeca --- /dev/null +++ b/apps/frontend/src/assets/img/ripple-effect-bg2.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/frontend/src/libs/components/app/app.tsx b/apps/frontend/src/libs/components/app/app.tsx index aa963167b..b8d469fd1 100644 --- a/apps/frontend/src/libs/components/app/app.tsx +++ b/apps/frontend/src/libs/components/app/app.tsx @@ -1,5 +1,10 @@ import reactLogo from "~/assets/img/react.svg"; -import { Link, Loader, RouterOutlet } from "~/libs/components/components.js"; +import { + Link, + Loader, + Notification, + RouterOutlet, +} from "~/libs/components/components.js"; import { AppRoute, DataStatus } from "~/libs/enums/enums.js"; import { useAppDispatch, @@ -61,6 +66,7 @@ const App: React.FC = () => { )} + ); }; diff --git a/apps/frontend/src/libs/components/button/button.tsx b/apps/frontend/src/libs/components/button/button.tsx index 6c6cea05d..4ea9dbeac 100644 --- a/apps/frontend/src/libs/components/button/button.tsx +++ b/apps/frontend/src/libs/components/button/button.tsx @@ -1,11 +1,27 @@ +import { getValidClassNames } from "~/libs/helpers/helpers.js"; + +import styles from "./styles.module.css"; + type Properties = { + isPrimary?: boolean; label: string; type?: "button" | "submit"; }; const Button: React.FC = ({ + isPrimary = true, label, type = "button", -}: Properties) => ; +}: Properties) => ( + +); export { Button }; diff --git a/apps/frontend/src/libs/components/button/styles.module.css b/apps/frontend/src/libs/components/button/styles.module.css new file mode 100644 index 000000000..5cc1022b7 --- /dev/null +++ b/apps/frontend/src/libs/components/button/styles.module.css @@ -0,0 +1,19 @@ +.btn { + display: inline-block; + min-height: 16px; + padding: 12px 24px; + margin: 0; + font-weight: 700; + line-height: 16px; + color: var(--black); + text-align: center; + cursor: pointer; + border: none; + border-radius: 35px; + outline: 0; +} + +.primary { + color: var(--white); + background-color: var(--black); +} diff --git a/apps/frontend/src/libs/components/components.ts b/apps/frontend/src/libs/components/components.ts index 018f79ecf..cd31e16b8 100644 --- a/apps/frontend/src/libs/components/components.ts +++ b/apps/frontend/src/libs/components/components.ts @@ -3,6 +3,7 @@ export { Button } from "./button/button.js"; export { Input } from "./input/input.js"; export { Link } from "./link/link.js"; export { Loader } from "./loader/loader.js"; +export { Notification } from "./notification/notification.js"; export { ProtectedRoute } from "./protected-route/protected-route.js"; export { RouterProvider } from "./router-provider/router-provider.js"; export { Provider as StoreProvider } from "react-redux"; diff --git a/apps/frontend/src/libs/components/input/input.tsx b/apps/frontend/src/libs/components/input/input.tsx index 108a8eedc..f816b9ced 100644 --- a/apps/frontend/src/libs/components/input/input.tsx +++ b/apps/frontend/src/libs/components/input/input.tsx @@ -5,8 +5,11 @@ import { type FieldValues, } from "react-hook-form"; +import { getValidClassNames } from "~/libs/helpers/helpers.js"; import { useFormController } from "~/libs/hooks/hooks.js"; +import styles from "./styles.module.css"; + type Properties = { control: Control; errors: FieldErrors; @@ -30,10 +33,20 @@ const Input = ({ const hasError = Boolean(error); return ( -