diff --git a/api/src/app/index.ts b/api/src/app/index.ts index b811c702f..4057f66ab 100644 --- a/api/src/app/index.ts +++ b/api/src/app/index.ts @@ -36,11 +36,11 @@ const app: Application = createExpressServer(routingControllersOptions); // Load old code to the app, temporarily until we migrate all endpoints app.use("/", router); -const { ENV, PORT } = Container.get(ConfigService).env(); +const { NODE_ENV, PORT } = Container.get(ConfigService).env(); const logger = Container.get(LoggerService); // Start it app.listen(PORT, () => { - const commonConfig = fsConfig(ENV); + const commonConfig = fsConfig(NODE_ENV); logger.info({ message: `API Server up on: ${commonConfig.api.url}/v2/docs` }); }); diff --git a/api/src/app/middlewares/error.ts b/api/src/app/middlewares/error.ts index af257d904..fae4c09f0 100644 --- a/api/src/app/middlewares/error.ts +++ b/api/src/app/middlewares/error.ts @@ -3,7 +3,7 @@ import { Middleware, } from "routing-controllers"; import { ErrorRequestHandler } from "express"; -import { GeneralResponseDto } from "../types"; +import { GeneralResponseDto } from "@dzcode.io/common/dist/types/api-responses"; import { LoggerService } from "../../logger/service"; import { Service } from "typedi"; diff --git a/api/src/app/middlewares/security.ts b/api/src/app/middlewares/security.ts index dcb257b96..b0fd14505 100644 --- a/api/src/app/middlewares/security.ts +++ b/api/src/app/middlewares/security.ts @@ -1,7 +1,7 @@ import { ExpressMiddlewareInterface, Middleware } from "routing-controllers"; import { RequestHandler, Router } from "express"; - import { ConfigService } from "../../config/service"; +import { CorsOptions } from "cors"; import { Service } from "typedi"; import helmet from "helmet"; import rateLimit from "express-rate-limit"; @@ -10,6 +10,16 @@ import rateLimit from "express-rate-limit"; @Middleware({ type: "before" }) export class SecurityMiddleware implements ExpressMiddlewareInterface { constructor(private configService: ConfigService) { + const env = this.configService.env().NODE_ENV; + this.whitelist = + env === "development" + ? ["http://localhost:8080"] + : env === "staging" + ? ["https://stage.dzcode.io"] + : env === "production" + ? ["https://www.dzcode.io"] + : []; + this.router.use(helmet()); this.router.use( @@ -21,20 +31,19 @@ export class SecurityMiddleware implements ExpressMiddlewareInterface { } private router = Router(); + private whitelist: string[]; use: RequestHandler = this.router; - public cors = () => { - const env = this.configService.env().ENV; + public cors = (): CorsOptions => { return { - origin: - env === "development" - ? ["http://localhost:8080"] - : env === "staging" - ? ["https://stage.dzcode.io"] - : env === "production" - ? ["https://www.dzcode.io"] - : true, + origin: (origin, callback) => { + if (!origin || this.whitelist.indexOf(origin) !== -1) { + callback(null, true); + } else { + callback(new Error(`Origin ${origin} not allowed by CORS`)); + } + }, }; }; } diff --git a/api/src/app/types/index.ts b/api/src/app/types/index.ts deleted file mode 100644 index 2ad143283..000000000 --- a/api/src/app/types/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"; - -export class GeneralResponseDto { - @IsNumber() - @IsOptional() - code?: number; - - @IsString() - @IsOptional() - msg?: string; - - @IsObject() - @IsOptional() - debug?: Record; -} diff --git a/api/src/config/dto.ts b/api/src/config/dto.ts index 8101419bf..04b8a44d9 100644 --- a/api/src/config/dto.ts +++ b/api/src/config/dto.ts @@ -7,5 +7,5 @@ export class ENVDto { PORT = 7070; @Matches("(" + environment.join(")|(") + ")") - ENV: Environment = "development"; + NODE_ENV: Environment = "development"; } diff --git a/api/src/config/service.spec.ts b/api/src/config/service.spec.ts index 7eca7c79d..25d5c4572 100644 --- a/api/src/config/service.spec.ts +++ b/api/src/config/service.spec.ts @@ -4,20 +4,29 @@ import dotenv from "dotenv"; jest.mock("dotenv"); const mockedDotenv = dotenv as jest.Mocked; +let OLD_ENV: NodeJS.ProcessEnv; describe("ConfigService", () => { + beforeAll(() => { + OLD_ENV = Object.assign({}, process.env); + }); + afterAll(() => { + process.env = OLD_ENV; + }); + it("should throw error when .env has invalid key value pair", async () => { - mockedDotenv.config.mockReturnValue({ parsed: { ENV: "testing" } }); + mockedDotenv.config.mockReturnValue({ parsed: { NODE_ENV: "testing" } }); expect(() => new ConfigService()).toThrowError( - `⚠️ Errors in .env file in the following keys:\nENV : {\"matches\":\"ENV must match (development)|(staging)|(production) regular expression\"}`, + `⚠️ Errors in .env file in the following keys:\nNODE_ENV : {\"matches\":\"NODE_ENV must match (development)|(staging)|(production) regular expression\"}`, ); }); it("should return default envs when .env is empty or doesn't exists", async () => { mockedDotenv.config.mockReturnValue({ error: new Error("test-error") }); + process.env = { NODE_ENV: "development" }; const configService = new ConfigService(); expect(configService).toBeInstanceOf(ConfigService); - expect(configService.env()).toMatchObject({ ENV: "development" }); + expect(configService.env()).toMatchObject({ NODE_ENV: "development" }); }); }); diff --git a/api/src/contributor/controller.spec.ts b/api/src/contributor/controller.spec.ts index 81db3b8de..c77195e3e 100644 --- a/api/src/contributor/controller.spec.ts +++ b/api/src/contributor/controller.spec.ts @@ -5,7 +5,7 @@ import { } from "../../test/mocks"; import { ContributorController } from "./controller"; -import { GetContributorsResponseDto } from "./types"; +import { GetContributorsResponseDto } from "@dzcode.io/common/dist/types/api-responses"; import { GithubService } from "../github/service"; import { mock } from "jest-mock-extended"; diff --git a/api/src/contributor/controller.ts b/api/src/contributor/controller.ts index b436f960e..e49c33a4e 100644 --- a/api/src/contributor/controller.ts +++ b/api/src/contributor/controller.ts @@ -1,8 +1,10 @@ import { Controller, Get, QueryParam } from "routing-controllers"; +import { + GetContributorsResponseDto, + GithubUserDto, +} from "@dzcode.io/common/dist/types/api-responses"; import { OpenAPI, ResponseSchema } from "routing-controllers-openapi"; -import { GetContributorsResponseDto } from "./types"; import { GithubService } from "../github/service"; -import { GithubUserDto } from "../github/dto"; import { Service } from "typedi"; @Service() diff --git a/api/src/contributor/types.ts b/api/src/contributor/types.ts deleted file mode 100644 index 0dc1e67cd..000000000 --- a/api/src/contributor/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {} from "module"; -import { GeneralResponseDto } from "../app/types"; -import { GithubUserDto } from "../github/dto"; -import { Type } from "class-transformer"; -import { ValidateNested } from "class-validator"; - -export class GetContributorsResponseDto extends GeneralResponseDto { - @ValidateNested({ each: true }) - @Type(() => GithubUserDto) - contributors?: GithubUserDto[]; -} diff --git a/api/src/github-user/controller.ts b/api/src/github-user/controller.ts index 7ec059088..fd2494fd3 100644 --- a/api/src/github-user/controller.ts +++ b/api/src/github-user/controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Param } from "routing-controllers"; import { OpenAPI, ResponseSchema } from "routing-controllers-openapi"; -import { GetUserResponseDto } from "./types"; +import { GetUserResponseDto } from "@dzcode.io/common/dist/types/api-responses"; import { GithubService } from "../github/service"; import { Service } from "typedi"; diff --git a/api/src/github-user/types.ts b/api/src/github-user/types.ts deleted file mode 100644 index 380a56d52..000000000 --- a/api/src/github-user/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { GeneralResponseDto } from "../app/types"; -import { GithubUserDto } from "../github/dto"; -import { ValidateNested } from "class-validator"; - -export class GetUserResponseDto extends GeneralResponseDto { - @ValidateNested() - user?: GithubUserDto; -} diff --git a/api/src/github/dto.ts b/api/src/github/dto.ts deleted file mode 100644 index 9197ac18b..000000000 --- a/api/src/github/dto.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { IsNumber, IsString } from "class-validator"; -import { GithubUser } from "@dzcode.io/common/dist/types"; - -export class GithubUserDto implements GithubUser { - @IsString() - avatar_url!: string; // eslint-disable-line camelcase - @IsString() - html_url!: string; // eslint-disable-line camelcase - @IsNumber() - id!: number; - @IsString() - login!: string; - @IsString() - type!: string; -} diff --git a/api/src/github/service.ts b/api/src/github/service.ts index 75413d838..caef4566c 100644 --- a/api/src/github/service.ts +++ b/api/src/github/service.ts @@ -4,7 +4,6 @@ import { GitHubUserApiResponse, ListContributorsResponse, } from "./types"; - import { Service } from "typedi"; import axios from "axios"; diff --git a/common/package.json b/common/package.json index 3968987b3..b6d0c1428 100644 --- a/common/package.json +++ b/common/package.json @@ -49,6 +49,9 @@ "typescript": "^4.1.3" }, "dependencies": { - "fs-extra": "^9.0.1" + "class-transformer": "^0.4.0", + "class-validator": "^0.13.1", + "fs-extra": "^9.0.1", + "reflect-metadata": "^0.1.13" } } diff --git a/common/src/types/api-responses.ts b/common/src/types/api-responses.ts new file mode 100644 index 000000000..9d665f950 --- /dev/null +++ b/common/src/types/api-responses.ts @@ -0,0 +1,43 @@ +import "reflect-metadata"; +import { IsNumber, IsObject, IsOptional, IsString } from "class-validator"; +import { GithubUser } from "."; +import { Type } from "class-transformer"; +import { ValidateNested } from "class-validator"; + +export class GeneralResponseDto { + @IsNumber() + @IsOptional() + code?: number; + + @IsString() + @IsOptional() + msg?: string; + + @IsObject() + @IsOptional() + debug?: Record; +} + +export class GithubUserDto implements GithubUser { + @IsString() + avatar_url!: string; // eslint-disable-line camelcase + @IsString() + html_url!: string; // eslint-disable-line camelcase + @IsNumber() + id!: number; + @IsString() + login!: string; + @IsString() + type!: string; +} + +export class GetContributorsResponseDto extends GeneralResponseDto { + @ValidateNested({ each: true }) + @Type(() => GithubUserDto) + contributors!: GithubUserDto[]; +} + +export class GetUserResponseDto extends GeneralResponseDto { + @ValidateNested() + user!: GithubUserDto; +} diff --git a/web/package.json b/web/package.json index 273a0d3d3..2749f3068 100644 --- a/web/package.json +++ b/web/package.json @@ -87,6 +87,8 @@ "babel-loader": "^8.2.2", "babel-plugin-import": "^1.13.3", "base64-inline-loader": "^1.1.1", + "class-transformer": "^0.4.0", + "class-validator": "^0.13.1", "cpx": "^1.5.0", "cross-env": "^7.0.3", "css-loader": "^5.0.1", @@ -122,6 +124,7 @@ "react-syntax-highlighter": "^15.4.3", "redux": "^4.0.5", "redux-thunk": "^2.3.0", + "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "sass": "^1.30.0", "sass-loader": "^10.1.0", diff --git a/web/src/apps/main/redux/actions/articles-page/index.ts b/web/src/apps/main/redux/actions/articles-page/index.ts index f14be9f42..98c2bbf8f 100644 --- a/web/src/apps/main/redux/actions/articles-page/index.ts +++ b/web/src/apps/main/redux/actions/articles-page/index.ts @@ -1,5 +1,8 @@ -import { Article, GithubUser } from "@dzcode.io/common/dist/types"; - +import { + GetContributorsResponseDto, + GetUserResponseDto, +} from "@dzcode.io/common/dist/types/api-responses"; +import { Article } from "@dzcode.io/common/dist/types"; import { ArticlesPageState } from "src/apps/main/redux/reducers/articles-page"; import { ArticlesState } from "src/apps/main/redux/reducers/articles"; import Axios from "axios"; @@ -60,7 +63,7 @@ export const fetchCurrentArticleContributors = (): ThunkResult< const { currentArticle } = getState().articlesPage; if (!currentArticle || Array.isArray(currentArticle.contributors)) return; - const response = await Axios.get( + const response = await Axios.get( apiURL + `/v2/contributors?path=articles/${currentArticle.slug}`, ); @@ -68,7 +71,7 @@ export const fetchCurrentArticleContributors = (): ThunkResult< throw Error("error_fetching_contributors"); } - const contributors = response.data; + const { contributors } = response.data; // getting the most recent current article const mrCurrentArticle = @@ -98,11 +101,13 @@ const fetchCurrentArticleAuthors = (): ThunkResult< const githubAuthors = ( await Promise.all( currentArticle.authors?.map((author) => { - return Axios.get(apiURL + `/v2/GithubUsers/${author}`); + return Axios.get( + apiURL + `/v2/GithubUsers/${author}`, + ); }) || [], ) ).map((response) => { - return response.data; + return response.data.user; }); // getting the most recent current article diff --git a/web/src/apps/main/redux/actions/documentation-page/index.ts b/web/src/apps/main/redux/actions/documentation-page/index.ts index 00351835d..5319b916e 100644 --- a/web/src/apps/main/redux/actions/documentation-page/index.ts +++ b/web/src/apps/main/redux/actions/documentation-page/index.ts @@ -1,6 +1,9 @@ -import { Document, GithubUser } from "@dzcode.io/common/dist/types"; - +import { + GetContributorsResponseDto, + GetUserResponseDto, +} from "@dzcode.io/common/dist/types/api-responses"; import Axios from "axios"; +import { Document } from "@dzcode.io/common/dist/types"; import { DocumentationState } from "src/apps/main/redux/reducers/documentation"; import { LearnPageState } from "src/apps/main/redux/reducers/learn-page"; import { SidebarTreeItem } from "src/apps/main/types"; @@ -60,7 +63,7 @@ const fetchCurrentDocumentContributors = (): ThunkResult< if (!currentDocument || Array.isArray(currentDocument.contributors)) return; - const response = await Axios.get( + const response = await Axios.get( apiURL + `/v2/contributors?path=documentation/${currentDocument.slug}`, ); @@ -68,7 +71,7 @@ const fetchCurrentDocumentContributors = (): ThunkResult< throw Error("error_fetching_contributors"); } - const contributors = response.data; + const { contributors } = response.data; const mrCurrentDocument = getState().learnPage.currentDocument || currentDocument; @@ -97,11 +100,13 @@ const fetchCurrentDocumentAuthors = (): ThunkResult< const githubAuthors = ( await Promise.all( currentDocument.authors?.map((author) => { - return Axios.get(apiURL + `/v2/GithubUsers/${author}`); + return Axios.get( + apiURL + `/v2/GithubUsers/${author}`, + ); }) || [], ) ).map((response) => { - return response.data; + return response.data.user; }); // getting the most recent current article