From e83341f7c2d615281957e47fe0ad9a23a55b4793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 08:57:48 +0100 Subject: [PATCH 01/13] Moves errors to common --- src/app/api/user/projects/route.ts | 2 +- src/app/layout.tsx | 2 +- src/common/{errorHandling => errors}/client/ErrorHandler.tsx | 0 .../auth/domain/AuthError.ts => common/errors/index.ts} | 0 src/common/session/Auth0Session.ts | 2 +- src/features/auth/data/Auth0RefreshTokenReader.ts | 2 +- src/features/auth/data/GitHubOAuthTokenRefresher.ts | 2 +- src/features/auth/domain/OAuthTokenRepository.ts | 2 +- src/features/auth/domain/SessionOAuthTokenRepository.ts | 2 +- 9 files changed, 7 insertions(+), 7 deletions(-) rename src/common/{errorHandling => errors}/client/ErrorHandler.tsx (100%) rename src/{features/auth/domain/AuthError.ts => common/errors/index.ts} (100%) diff --git a/src/app/api/user/projects/route.ts b/src/app/api/user/projects/route.ts index a0ac3816..6e259994 100644 --- a/src/app/api/user/projects/route.ts +++ b/src/app/api/user/projects/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server" import { projectDataSource } from "@/composition" -import { UnauthorizedError } from "@/features/auth/domain/AuthError" +import { UnauthorizedError } from "@/common/errors" export async function GET() { try { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6d89e3d4..1760b426 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,7 +4,7 @@ import { UserProvider } from "@auth0/nextjs-auth0/client" import { config as fontAwesomeConfig } from "@fortawesome/fontawesome-svg-core" import { CssBaseline } from "@mui/material" import ThemeRegistry from "@/common/theme/ThemeRegistry" -import ErrorHandler from "@/common/errorHandling/client/ErrorHandler" +import ErrorHandler from "@/common/errors/client/ErrorHandler" import "@fortawesome/fontawesome-svg-core/styles.css" fontAwesomeConfig.autoAddCss = false diff --git a/src/common/errorHandling/client/ErrorHandler.tsx b/src/common/errors/client/ErrorHandler.tsx similarity index 100% rename from src/common/errorHandling/client/ErrorHandler.tsx rename to src/common/errors/client/ErrorHandler.tsx diff --git a/src/features/auth/domain/AuthError.ts b/src/common/errors/index.ts similarity index 100% rename from src/features/auth/domain/AuthError.ts rename to src/common/errors/index.ts diff --git a/src/common/session/Auth0Session.ts b/src/common/session/Auth0Session.ts index 4131b040..0971235b 100644 --- a/src/common/session/Auth0Session.ts +++ b/src/common/session/Auth0Session.ts @@ -1,6 +1,6 @@ import { getSession } from "@auth0/nextjs-auth0" +import { UnauthorizedError } from "@/common/errors" import ISession from "./ISession" -import { UnauthorizedError } from "@/features/auth/domain/AuthError" export default class Auth0Session implements ISession { async getUserId(): Promise { diff --git a/src/features/auth/data/Auth0RefreshTokenReader.ts b/src/features/auth/data/Auth0RefreshTokenReader.ts index 57bf2cde..79fd6bfb 100644 --- a/src/features/auth/data/Auth0RefreshTokenReader.ts +++ b/src/features/auth/data/Auth0RefreshTokenReader.ts @@ -1,6 +1,6 @@ import { ManagementClient } from "auth0" +import { UnauthorizedError } from "@/common/errors" import IRefreshTokenReader from "../domain/IRefreshTokenReader" -import { UnauthorizedError } from "../domain/AuthError" interface Auth0RefreshTokenReaderConfig { domain: string diff --git a/src/features/auth/data/GitHubOAuthTokenRefresher.ts b/src/features/auth/data/GitHubOAuthTokenRefresher.ts index 68dd12c4..d5e88607 100644 --- a/src/features/auth/data/GitHubOAuthTokenRefresher.ts +++ b/src/features/auth/data/GitHubOAuthTokenRefresher.ts @@ -1,6 +1,6 @@ +import { UnauthorizedError } from "@/common/errors" import OAuthToken from "../domain/OAuthToken" import IOAuthTokenRefresher from "../domain/IOAuthTokenRefresher" -import { UnauthorizedError } from "../domain/AuthError" export interface GitHubOAuthTokenRefresherConfig { readonly clientId: string diff --git a/src/features/auth/domain/OAuthTokenRepository.ts b/src/features/auth/domain/OAuthTokenRepository.ts index d8bdfe17..fd416386 100644 --- a/src/features/auth/domain/OAuthTokenRepository.ts +++ b/src/features/auth/domain/OAuthTokenRepository.ts @@ -1,7 +1,7 @@ import ZodJSONCoder from "@/common/utils/ZodJSONCoder" import IUserDataRepository from "@/common/userData/IUserDataRepository" +import { UnauthorizedError } from "@/common/errors" import IOAuthTokenRepository from "./IOAuthTokenRepository" -import { UnauthorizedError } from "./AuthError" import OAuthToken, { OAuthTokenSchema } from "./OAuthToken" export default class OAuthTokenRepository implements IOAuthTokenRepository { diff --git a/src/features/auth/domain/SessionOAuthTokenRepository.ts b/src/features/auth/domain/SessionOAuthTokenRepository.ts index c7942598..b4add594 100644 --- a/src/features/auth/domain/SessionOAuthTokenRepository.ts +++ b/src/features/auth/domain/SessionOAuthTokenRepository.ts @@ -1,7 +1,7 @@ +import { UnauthorizedError } from "@/common/errors" import ZodJSONCoder from "../../../common/utils/ZodJSONCoder" import ISessionDataRepository from "@/common/userData/ISessionDataRepository" import ISessionOAuthTokenRepository from "./SessionOAuthTokenRepository" -import { UnauthorizedError } from "./AuthError" import OAuthToken, { OAuthTokenSchema } from "./OAuthToken" export default class SessionOAuthTokenRepository implements ISessionOAuthTokenRepository { From a477edac4acf0fbcfb4f33f262282f30af046ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:49:06 +0100 Subject: [PATCH 02/13] Extends GitHubClient to get org membership status --- .../AccessTokenRefreshingGitHubClient.ts | 12 +++++- src/common/github/GitHubClient.ts | 37 ++++++++++++++++++- src/common/github/IGitHubClient.ts | 13 +++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/common/github/AccessTokenRefreshingGitHubClient.ts b/src/common/github/AccessTokenRefreshingGitHubClient.ts index e9a3a29a..4d5ea36d 100644 --- a/src/common/github/AccessTokenRefreshingGitHubClient.ts +++ b/src/common/github/AccessTokenRefreshingGitHubClient.ts @@ -4,9 +4,11 @@ import IGitHubClient, { GraphQlQueryResponse, GetRepositoryContentRequest, GetPullRequestCommentsRequest, + GetOrganizationMembershipStatusRequest, AddCommentToPullRequestRequest, RepositoryContent, - PullRequestComment + PullRequestComment, + OrganizationMembershipStatus } from "./IGitHubClient" const HttpErrorSchema = z.object({ @@ -60,6 +62,14 @@ export default class AccessTokenRefreshingGitHubClient implements IGitHubClient }) } + async getOrganizationMembershipStatus( + request: GetOrganizationMembershipStatusRequest + ): Promise { + return await this.send(async () => { + return await this.gitHubClient.getOrganizationMembershipStatus(request) + }) + } + private async send(fn: () => Promise): Promise { const accessToken = await this.accessTokenReader.getAccessToken() try { diff --git a/src/common/github/GitHubClient.ts b/src/common/github/GitHubClient.ts index 2b03cbe2..843c802d 100644 --- a/src/common/github/GitHubClient.ts +++ b/src/common/github/GitHubClient.ts @@ -6,8 +6,10 @@ import IGitHubClient, { GetRepositoryContentRequest, GetPullRequestCommentsRequest, AddCommentToPullRequestRequest, + GetOrganizationMembershipStatusRequest, RepositoryContent, - PullRequestComment + PullRequestComment, + OrganizationMembershipStatus } from "./IGitHubClient" type GitHubClientConfig = { @@ -90,4 +92,37 @@ export default class GitHubClient implements IGitHubClient { body: request.body }) } + + async getOrganizationMembershipStatus( + request: GetOrganizationMembershipStatusRequest + ): Promise { + const accessToken = await this.accessTokenReader.getAccessToken() + const octokit = new Octokit({ auth: accessToken }) + try { + const response = await octokit.rest.orgs.getMembershipForAuthenticatedUser({ + org: request.organizationName + }) + console.log(response) + if (response.data.state == "active") { + return OrganizationMembershipStatus.ACTIVE + } else if (response.data.state == "pending") { + return OrganizationMembershipStatus.PENDING + } else { + return OrganizationMembershipStatus.UNKNOWN + } + } catch (error: any) { + console.log(error) + if (error.status) { + if (error.status == 404) { + return OrganizationMembershipStatus.NOT_A_MEMBER + } else if (error.status == 403) { + return OrganizationMembershipStatus.GITHUB_APP_BLOCKED + } else { + throw error + } + } else { + throw error + } + } + } } diff --git a/src/common/github/IGitHubClient.ts b/src/common/github/IGitHubClient.ts index 9537786e..a66e689f 100644 --- a/src/common/github/IGitHubClient.ts +++ b/src/common/github/IGitHubClient.ts @@ -40,9 +40,22 @@ export type AddCommentToPullRequestRequest = { readonly body: string } +export type GetOrganizationMembershipStatusRequest = { + readonly organizationName: string +} + +export enum OrganizationMembershipStatus { + NOT_A_MEMBER = "not_a_member", + ACTIVE = "active", + PENDING = "pending", + GITHUB_APP_BLOCKED = "github_app_blocked", + UNKNOWN = "unknown" +} + export default interface IGitHubClient { graphql(request: GraphQLQueryRequest): Promise getRepositoryContent(request: GetRepositoryContentRequest): Promise getPullRequestComments(request: GetPullRequestCommentsRequest): Promise addCommentToPullRequest(request: AddCommentToPullRequestRequest): Promise + getOrganizationMembershipStatus(request: GetOrganizationMembershipStatusRequest): Promise } From 2681ef78000bddd2811759ccf31e675bb63caf89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:49:36 +0100 Subject: [PATCH 03/13] Throws InvalidSessionError when fetching projects with invalid session --- src/common/errors/index.ts | 1 + .../GitHubOrganizationSessionValidator.ts | 19 ++++++++++++++ src/common/session/ISessionValidator.ts | 3 +++ .../SessionValidatingProjectDataSource.ts | 25 +++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 src/common/session/GitHubOrganizationSessionValidator.ts create mode 100644 src/common/session/ISessionValidator.ts create mode 100644 src/features/projects/domain/SessionValidatingProjectDataSource.ts diff --git a/src/common/errors/index.ts b/src/common/errors/index.ts index 2e482071..74acb8d0 100644 --- a/src/common/errors/index.ts +++ b/src/common/errors/index.ts @@ -1 +1,2 @@ export class UnauthorizedError extends Error {} +export class InvalidSessionError extends Error {} diff --git a/src/common/session/GitHubOrganizationSessionValidator.ts b/src/common/session/GitHubOrganizationSessionValidator.ts new file mode 100644 index 00000000..837bc030 --- /dev/null +++ b/src/common/session/GitHubOrganizationSessionValidator.ts @@ -0,0 +1,19 @@ +import IGitHubClient, { OrganizationMembershipStatus } from "../github/IGitHubClient" +import ISessionValidator from "./ISessionValidator" + +export default class GitHubOrganizationSessionValidator implements ISessionValidator { + private readonly gitHubClient: IGitHubClient + private readonly acceptedOrganization: string + + constructor(gitHubClient: IGitHubClient, acceptedOrganization: string) { + this.gitHubClient = gitHubClient + this.acceptedOrganization = acceptedOrganization + } + + async validateSession(): Promise { + const status = await this.gitHubClient.getOrganizationMembershipStatus({ + organizationName: this.acceptedOrganization + }) + return status == OrganizationMembershipStatus.ACTIVE + } +} diff --git a/src/common/session/ISessionValidator.ts b/src/common/session/ISessionValidator.ts new file mode 100644 index 00000000..b51cd5e6 --- /dev/null +++ b/src/common/session/ISessionValidator.ts @@ -0,0 +1,3 @@ +export default interface ISessionValidator { + validateSession(): Promise +} diff --git a/src/features/projects/domain/SessionValidatingProjectDataSource.ts b/src/features/projects/domain/SessionValidatingProjectDataSource.ts new file mode 100644 index 00000000..2ccc96a1 --- /dev/null +++ b/src/features/projects/domain/SessionValidatingProjectDataSource.ts @@ -0,0 +1,25 @@ +import { InvalidSessionError } from "@/common/errors" +import ISessionValidator from "@/common/session/ISessionValidator" +import IProjectDataSource from "../domain/IProjectDataSource" +import Project from "../domain/Project" + +export default class SessionValidatingProjectDataSource implements IProjectDataSource { + private readonly sessionValidator: ISessionValidator + private readonly projectDataSource: IProjectDataSource + + constructor( + sessionValidator: ISessionValidator, + projectDataSource: IProjectDataSource + ) { + this.sessionValidator = sessionValidator + this.projectDataSource = projectDataSource + } + + async getProjects(): Promise { + const isValid = await this.sessionValidator.validateSession() + if (!isValid) { + throw new InvalidSessionError() + } + return await this.projectDataSource.getProjects() + } +} From ff014c33acd577d3ba153fa5804943d280601758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:49:53 +0100 Subject: [PATCH 04/13] Shows invalid session page when catching InvalidSessionError --- src/app/api/user/projects/route.ts | 23 +++++++------- src/app/invalid-session/page.tsx | 5 ++++ src/common/errors/client/ErrorHandler.tsx | 9 ++++-- src/features/auth/view/InvalidSessionPage.tsx | 12 ++++++++ .../auth/view/client/InvalidSession.tsx | 30 +++++++++++++++++++ 5 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 src/app/invalid-session/page.tsx create mode 100644 src/features/auth/view/InvalidSessionPage.tsx create mode 100644 src/features/auth/view/client/InvalidSession.tsx diff --git a/src/app/api/user/projects/route.ts b/src/app/api/user/projects/route.ts index 6e259994..799f13f6 100644 --- a/src/app/api/user/projects/route.ts +++ b/src/app/api/user/projects/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server" import { projectDataSource } from "@/composition" -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError, InvalidSessionError } from "@/common/errors" export async function GET() { try { @@ -8,20 +8,17 @@ export async function GET() { return NextResponse.json({projects}) } catch (error) { if (error instanceof UnauthorizedError) { - return NextResponse.json({ - status: 401, - message: error.message - }, { status: 401 }) + return errorResponse(401, error.message) + } else if (error instanceof InvalidSessionError) { + return errorResponse(403, error.message) } else if (error instanceof Error) { - return NextResponse.json({ - status: 500, - message: error.message - }, { status: 500 }) + return errorResponse(500, error.message) } else { - return NextResponse.json({ - status: 500, - message: "Unknown error" - }, { status: 500 }) + return errorResponse(500, "Unknown error") } } } + +function errorResponse(status: number, message: string): NextResponse { + return NextResponse.json({ status, message }, { status }) +} diff --git a/src/app/invalid-session/page.tsx b/src/app/invalid-session/page.tsx new file mode 100644 index 00000000..ee33fec5 --- /dev/null +++ b/src/app/invalid-session/page.tsx @@ -0,0 +1,5 @@ +import InvalidSessionPage from "@/features/auth/view/InvalidSessionPage" + +export default async function Page() { + return +} diff --git a/src/common/errors/client/ErrorHandler.tsx b/src/common/errors/client/ErrorHandler.tsx index 83b68d45..20ba19a3 100644 --- a/src/common/errors/client/ErrorHandler.tsx +++ b/src/common/errors/client/ErrorHandler.tsx @@ -9,10 +9,13 @@ export default function ErrorHandler({ children: React.ReactNode }) { const onSWRError = (error: FetcherError) => { + if (typeof window === "undefined") { + return + } if (error.status == 401) { - if (typeof window !== "undefined") { - window.location.href = "/api/auth/logout" - } + window.location.href = "/api/auth/logout" + } else if (error.status == 403) { + window.location.href = "/invalid-session" } } return ( diff --git a/src/features/auth/view/InvalidSessionPage.tsx b/src/features/auth/view/InvalidSessionPage.tsx new file mode 100644 index 00000000..ec99a970 --- /dev/null +++ b/src/features/auth/view/InvalidSessionPage.tsx @@ -0,0 +1,12 @@ +import { redirect } from "next/navigation" +import { sessionValidator } from "@/composition" +import InvalidSession from "./client/InvalidSession" + +export default async function InvalidSessionPage() { + const isSessionValid = await sessionValidator.validateSession() + if (isSessionValid) { + // User ended up here by mistake so lets send them to the front page. + redirect("/") + } + return +} diff --git a/src/features/auth/view/client/InvalidSession.tsx b/src/features/auth/view/client/InvalidSession.tsx new file mode 100644 index 00000000..01a5b95f --- /dev/null +++ b/src/features/auth/view/client/InvalidSession.tsx @@ -0,0 +1,30 @@ +"use client" + +import { Box, Button, Typography } from "@mui/material" + +export default function InvalidSession() { + const navigateToFrontPage = () => { + if (typeof window !== "undefined") { + window.location.href = "/api/auth/logout" + } + } + const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE + return ( + + + {`Your account does not have access to ${siteName}.`} + + + + ) +} From 1ab92ea57029adc65401cec54bbc4f0c00076834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:50:02 +0100 Subject: [PATCH 05/13] Adds session validation to composition --- src/composition.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/composition.ts b/src/composition.ts index 37a60313..dcfac50c 100644 --- a/src/composition.ts +++ b/src/composition.ts @@ -4,6 +4,7 @@ import Auth0Session from "@/common/session/Auth0Session" import CachingProjectDataSource from "@/features/projects/domain/CachingProjectDataSource" import GitHubClient from "@/common/github/GitHubClient" import GitHubOAuthTokenRefresher from "@/features/auth/data/GitHubOAuthTokenRefresher" +import GitHubOrganizationSessionValidator from "@/common/session/GitHubOrganizationSessionValidator" import GitHubProjectDataSource from "@/features/projects/data/GitHubProjectDataSource" import InitialOAuthTokenService from "@/features/auth/domain/InitialOAuthTokenService" import KeyValueUserDataRepository from "@/common/userData/KeyValueUserDataRepository" @@ -15,6 +16,7 @@ import SessionDataRepository from "@/common/userData/SessionDataRepository" import SessionMutexFactory from "@/common/mutex/SessionMutexFactory" import SessionOAuthTokenRepository from "@/features/auth/domain/SessionOAuthTokenRepository" import SessionProjectRepository from "@/features/projects/domain/SessionProjectRepository" +import SessionValidatingProjectDataSource from "@/features/projects/domain/SessionValidatingProjectDataSource" import OAuthTokenRepository from "@/features/auth/domain/OAuthTokenRepository" import authLogoutHandler from "@/common/authHandler/logout" @@ -70,6 +72,11 @@ export const gitHubClient = new AccessTokenRefreshingGitHubClient( }) ) +export const sessionValidator = new GitHubOrganizationSessionValidator( + gitHubClient, + GITHUB_ORGANIZATION_NAME +) + export const sessionProjectRepository = new SessionProjectRepository( new SessionDataRepository( new Auth0Session(), @@ -81,9 +88,12 @@ export const sessionProjectRepository = new SessionProjectRepository( ) export const projectDataSource = new CachingProjectDataSource( - new GitHubProjectDataSource( - gitHubClient, - GITHUB_ORGANIZATION_NAME + new SessionValidatingProjectDataSource( + sessionValidator, + new GitHubProjectDataSource( + gitHubClient, + GITHUB_ORGANIZATION_NAME + ) ), sessionProjectRepository ) From 3e23c87b22874bf061ada2ef6b43a04378959278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:54:41 +0100 Subject: [PATCH 06/13] Removes debug logs --- src/common/github/GitHubClient.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/github/GitHubClient.ts b/src/common/github/GitHubClient.ts index 843c802d..d60748ec 100644 --- a/src/common/github/GitHubClient.ts +++ b/src/common/github/GitHubClient.ts @@ -102,7 +102,6 @@ export default class GitHubClient implements IGitHubClient { const response = await octokit.rest.orgs.getMembershipForAuthenticatedUser({ org: request.organizationName }) - console.log(response) if (response.data.state == "active") { return OrganizationMembershipStatus.ACTIVE } else if (response.data.state == "pending") { @@ -111,7 +110,6 @@ export default class GitHubClient implements IGitHubClient { return OrganizationMembershipStatus.UNKNOWN } } catch (error: any) { - console.log(error) if (error.status) { if (error.status == 404) { return OrganizationMembershipStatus.NOT_A_MEMBER From b635f77377569ae306cd949b03d128f43276312b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:56:04 +0100 Subject: [PATCH 07/13] Fixes linting error --- src/common/github/GitHubClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/github/GitHubClient.ts b/src/common/github/GitHubClient.ts index d60748ec..dba5c281 100644 --- a/src/common/github/GitHubClient.ts +++ b/src/common/github/GitHubClient.ts @@ -109,6 +109,7 @@ export default class GitHubClient implements IGitHubClient { } else { return OrganizationMembershipStatus.UNKNOWN } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ } catch (error: any) { if (error.status) { if (error.status == 404) { From aaeba6c3b51fb67531e5f500ff547c1df3b09589 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 09:58:39 +0100 Subject: [PATCH 08/13] Fixes unit tests --- .../AccessTokenRefreshingGitHubClient.test.ts | 36 +++++++++++++++---- .../domain/SessionOAuthTokenRepository.ts | 2 +- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts index c8c7d21a..db6d05c3 100644 --- a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts +++ b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts @@ -3,7 +3,8 @@ import { GraphQLQueryRequest, GetRepositoryContentRequest, GetPullRequestCommentsRequest, - AddCommentToPullRequestRequest + AddCommentToPullRequestRequest, + OrganizationMembershipStatus } from "../../../src/common/github/IGitHubClient" test("It forwards a GraphQL request", async () => { @@ -27,7 +28,10 @@ test("It forwards a GraphQL request", async () => { async getPullRequestComments() { return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GraphQLQueryRequest = { query: "foo", @@ -58,7 +62,10 @@ test("It forwards a request to get the repository content", async () => { async getPullRequestComments() { return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GetRepositoryContentRequest = { repositoryOwner: "foo", @@ -91,7 +98,10 @@ test("It forwards a request to get comments to a pull request", async () => { forwardedRequest = request return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GetPullRequestCommentsRequest = { appInstallationId: 1234, @@ -125,6 +135,9 @@ test("It forwards a request to add a comment to a pull request", async () => { }, async addCommentToPullRequest(request: AddCommentToPullRequestRequest) { forwardedRequest = request + }, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN } }) const request: AddCommentToPullRequestRequest = { @@ -164,7 +177,10 @@ test("It retries with a refreshed access token when receiving HTTP 401", async ( async getPullRequestComments() { return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GraphQLQueryRequest = { query: "foo", @@ -195,7 +211,10 @@ test("It only retries a request once when receiving HTTP 401", async () => { async getPullRequestComments() { return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GraphQLQueryRequest = { query: "foo", @@ -229,7 +248,10 @@ test("It does not refresh an access token when the initial request was successfu async getPullRequestComments() { return [] }, - async addCommentToPullRequest() {} + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } }) const request: GraphQLQueryRequest = { query: "foo", diff --git a/src/features/auth/domain/SessionOAuthTokenRepository.ts b/src/features/auth/domain/SessionOAuthTokenRepository.ts index b4add594..2f5d0905 100644 --- a/src/features/auth/domain/SessionOAuthTokenRepository.ts +++ b/src/features/auth/domain/SessionOAuthTokenRepository.ts @@ -1,4 +1,4 @@ -import { UnauthorizedError } from "@/common/errors" +import { UnauthorizedError } from "../../../common/errors" import ZodJSONCoder from "../../../common/utils/ZodJSONCoder" import ISessionDataRepository from "@/common/userData/ISessionDataRepository" import ISessionOAuthTokenRepository from "./SessionOAuthTokenRepository" From 4f8794a16dd74f7b0d487f86426f822b81358fb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 10:53:44 +0100 Subject: [PATCH 09/13] Improves error message --- src/features/auth/view/InvalidSessionPage.tsx | 12 +++++- .../auth/view/client/InvalidSession.tsx | 40 ++++++++++--------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/features/auth/view/InvalidSessionPage.tsx b/src/features/auth/view/InvalidSessionPage.tsx index ec99a970..99292a13 100644 --- a/src/features/auth/view/InvalidSessionPage.tsx +++ b/src/features/auth/view/InvalidSessionPage.tsx @@ -2,11 +2,21 @@ import { redirect } from "next/navigation" import { sessionValidator } from "@/composition" import InvalidSession from "./client/InvalidSession" +const { + NEXT_PUBLIC_SHAPE_DOCS_TITLE, + GITHUB_ORGANIZATION_NAME +} = process.env + export default async function InvalidSessionPage() { const isSessionValid = await sessionValidator.validateSession() if (isSessionValid) { // User ended up here by mistake so lets send them to the front page. redirect("/") } - return + return ( + + ) } diff --git a/src/features/auth/view/client/InvalidSession.tsx b/src/features/auth/view/client/InvalidSession.tsx index 01a5b95f..d5fabd93 100644 --- a/src/features/auth/view/client/InvalidSession.tsx +++ b/src/features/auth/view/client/InvalidSession.tsx @@ -1,30 +1,32 @@ "use client" -import { Box, Button, Typography } from "@mui/material" +import { Box, Button, Stack, Typography } from "@mui/material" -export default function InvalidSession() { +export default function InvalidSession({ + siteName, + organizationName +}: { + siteName: string + organizationName: string +}) { const navigateToFrontPage = () => { if (typeof window !== "undefined") { window.location.href = "/api/auth/logout" } } - const siteName = process.env.NEXT_PUBLIC_SHAPE_DOCS_TITLE return ( - - - {`Your account does not have access to ${siteName}.`} - - - + + + + Your account does not have access to {siteName} + + + Access to {siteName} requires that your account is an active member of the {organizationName} organization on GitHub. + + + + ) } From f64e6de9b4b140ceaa94652f77e1aaa02c49c7ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 11:00:22 +0100 Subject: [PATCH 10/13] Fixes linting warning --- src/features/auth/view/client/InvalidSession.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/auth/view/client/InvalidSession.tsx b/src/features/auth/view/client/InvalidSession.tsx index d5fabd93..76cf8416 100644 --- a/src/features/auth/view/client/InvalidSession.tsx +++ b/src/features/auth/view/client/InvalidSession.tsx @@ -1,6 +1,6 @@ "use client" -import { Box, Button, Stack, Typography } from "@mui/material" +import { Button, Stack, Typography } from "@mui/material" export default function InvalidSession({ siteName, From dd442941672f36f7f2def33cdcb00590f9f26780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 11:10:35 +0100 Subject: [PATCH 11/13] Adds tests for GitHubOrganizationSessionValidator --- ...GitHubOrganizationSessionValidator.test.ts | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 __test__/common/session/GitHubOrganizationSessionValidator.test.ts diff --git a/__test__/common/session/GitHubOrganizationSessionValidator.test.ts b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts new file mode 100644 index 00000000..2503e5f2 --- /dev/null +++ b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts @@ -0,0 +1,145 @@ +import { + GetOrganizationMembershipStatusRequest, + OrganizationMembershipStatus +} from "../../../src/common/github/IGitHubClient" +import GitHubOrganizationSessionValidator from "../../../src/common/session/GitHubOrganizationSessionValidator" + +test("It requests organization membership status for the specified organization", async () => { + let queriedOrganizationName: string | undefined + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus(request: GetOrganizationMembershipStatusRequest) { + queriedOrganizationName = request.organizationName + return OrganizationMembershipStatus.UNKNOWN + } + }, + "foo" + ) + await sut.validateSession() + expect(queriedOrganizationName).toBe("foo") +}) + +test("It considers session valid when membership status is \"Active\"", async () => { + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.ACTIVE + } + }, + "foo" + ) + const isSessionValid = await sut.validateSession() + expect(isSessionValid).toBeTruthy() +}) + +test("It considers session invalid when membership status is \"Not a member\"", async () => { + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.NOT_A_MEMBER + } + }, + "foo" + ) + const isSessionValid = await sut.validateSession() + expect(isSessionValid).toBeFalsy() +}) + +test("It considers session invalid when membership status is \"Pending\"", async () => { + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.PENDING + } + }, + "foo" + ) + const isSessionValid = await sut.validateSession() + expect(isSessionValid).toBeFalsy() +}) + +test("It considers session invalid when membership status is \"GitHub App Blocked\"", async () => { + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.GITHUB_APP_BLOCKED + } + }, + "foo" + ) + const isSessionValid = await sut.validateSession() + expect(isSessionValid).toBeFalsy() +}) + +test("It considers session invalid when membership status is \"Unknown\"", async () => { + const sut = new GitHubOrganizationSessionValidator( + { + async graphql() { + return {} + }, + async getRepositoryContent() { + return { downloadURL: "https://example.com" } + }, + async getPullRequestComments() { + return [] + }, + async addCommentToPullRequest() {}, + async getOrganizationMembershipStatus() { + return OrganizationMembershipStatus.UNKNOWN + } + }, + "foo" + ) + const isSessionValid = await sut.validateSession() + expect(isSessionValid).toBeFalsy() +}) \ No newline at end of file From 0c1436d2f6d4eab3c116048cafa68169a54b31a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 11:17:23 +0100 Subject: [PATCH 12/13] Adds tests for SessionValidatingProjectDataSource --- ...SessionValidatingProjectDataSource.test.ts | 46 +++++++++++++++++++ .../SessionValidatingProjectDataSource.ts | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 __test__/projects/SessionValidatingProjectDataSource.test.ts diff --git a/__test__/projects/SessionValidatingProjectDataSource.test.ts b/__test__/projects/SessionValidatingProjectDataSource.test.ts new file mode 100644 index 00000000..f0f96dc7 --- /dev/null +++ b/__test__/projects/SessionValidatingProjectDataSource.test.ts @@ -0,0 +1,46 @@ +import SessionValidatingProjectDataSource from "../../src/features/projects/domain/SessionValidatingProjectDataSource" + +test("It validates the session", async () => { + let didValidateSession = false + const sut = new SessionValidatingProjectDataSource({ + async validateSession() { + didValidateSession = true + return true + }, + }, { + async getProjects() { + return [] + } + }) + await sut.getProjects() + expect(didValidateSession).toBeTruthy() +}) + +test("It fetches projects when session is valid", async () => { + let didFetchProjects = false + const sut = new SessionValidatingProjectDataSource({ + async validateSession() { + return true + }, + }, { + async getProjects() { + didFetchProjects = true + return [] + } + }) + await sut.getProjects() + expect(didFetchProjects).toBeTruthy() +}) + +test("It throws error when session is invalid", async () => { + const sut = new SessionValidatingProjectDataSource({ + async validateSession() { + return false + }, + }, { + async getProjects() { + return [] + } + }) + expect(sut.getProjects()).rejects.toThrowError() +}) diff --git a/src/features/projects/domain/SessionValidatingProjectDataSource.ts b/src/features/projects/domain/SessionValidatingProjectDataSource.ts index 2ccc96a1..942d0859 100644 --- a/src/features/projects/domain/SessionValidatingProjectDataSource.ts +++ b/src/features/projects/domain/SessionValidatingProjectDataSource.ts @@ -1,4 +1,4 @@ -import { InvalidSessionError } from "@/common/errors" +import { InvalidSessionError } from "../../../common/errors" import ISessionValidator from "@/common/session/ISessionValidator" import IProjectDataSource from "../domain/IProjectDataSource" import Project from "../domain/Project" From fa84596f70afbe4cdbaafe67b36bf9ca453462d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Mon, 6 Nov 2023 11:36:18 +0100 Subject: [PATCH 13/13] Removes OrganizationMembershipStatus type --- .../AccessTokenRefreshingGitHubClient.test.ts | 17 +++++---- ...GitHubOrganizationSessionValidator.test.ts | 28 +++++++-------- .../AccessTokenRefreshingGitHubClient.ts | 6 ++-- src/common/github/GitHubClient.ts | 35 ++++--------------- src/common/github/IGitHubClient.ts | 10 ++---- .../GitHubOrganizationSessionValidator.ts | 25 ++++++++++--- 6 files changed, 54 insertions(+), 67 deletions(-) diff --git a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts index db6d05c3..7913e1c3 100644 --- a/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts +++ b/__test__/common/github/AccessTokenRefreshingGitHubClient.test.ts @@ -3,8 +3,7 @@ import { GraphQLQueryRequest, GetRepositoryContentRequest, GetPullRequestCommentsRequest, - AddCommentToPullRequestRequest, - OrganizationMembershipStatus + AddCommentToPullRequestRequest } from "../../../src/common/github/IGitHubClient" test("It forwards a GraphQL request", async () => { @@ -30,7 +29,7 @@ test("It forwards a GraphQL request", async () => { }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GraphQLQueryRequest = { @@ -64,7 +63,7 @@ test("It forwards a request to get the repository content", async () => { }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GetRepositoryContentRequest = { @@ -100,7 +99,7 @@ test("It forwards a request to get comments to a pull request", async () => { }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GetPullRequestCommentsRequest = { @@ -137,7 +136,7 @@ test("It forwards a request to add a comment to a pull request", async () => { forwardedRequest = request }, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: AddCommentToPullRequestRequest = { @@ -179,7 +178,7 @@ test("It retries with a refreshed access token when receiving HTTP 401", async ( }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GraphQLQueryRequest = { @@ -213,7 +212,7 @@ test("It only retries a request once when receiving HTTP 401", async () => { }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GraphQLQueryRequest = { @@ -250,7 +249,7 @@ test("It does not refresh an access token when the initial request was successfu }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }) const request: GraphQLQueryRequest = { diff --git a/__test__/common/session/GitHubOrganizationSessionValidator.test.ts b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts index 2503e5f2..105039de 100644 --- a/__test__/common/session/GitHubOrganizationSessionValidator.test.ts +++ b/__test__/common/session/GitHubOrganizationSessionValidator.test.ts @@ -1,6 +1,5 @@ import { - GetOrganizationMembershipStatusRequest, - OrganizationMembershipStatus + GetOrganizationMembershipStatusRequest } from "../../../src/common/github/IGitHubClient" import GitHubOrganizationSessionValidator from "../../../src/common/session/GitHubOrganizationSessionValidator" @@ -20,7 +19,7 @@ test("It requests organization membership status for the specified organization" async addCommentToPullRequest() {}, async getOrganizationMembershipStatus(request: GetOrganizationMembershipStatusRequest) { queriedOrganizationName = request.organizationName - return OrganizationMembershipStatus.UNKNOWN + return { state: "active" } } }, "foo" @@ -29,7 +28,7 @@ test("It requests organization membership status for the specified organization" expect(queriedOrganizationName).toBe("foo") }) -test("It considers session valid when membership status is \"Active\"", async () => { +test("It considers session valid when membership state is \"active\"", async () => { const sut = new GitHubOrganizationSessionValidator( { async graphql() { @@ -43,7 +42,7 @@ test("It considers session valid when membership status is \"Active\"", async () }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.ACTIVE + return { state: "active" } } }, "foo" @@ -52,7 +51,7 @@ test("It considers session valid when membership status is \"Active\"", async () expect(isSessionValid).toBeTruthy() }) -test("It considers session invalid when membership status is \"Not a member\"", async () => { +test("It considers session invalid when membership state is \"pending\"", async () => { const sut = new GitHubOrganizationSessionValidator( { async graphql() { @@ -66,7 +65,7 @@ test("It considers session invalid when membership status is \"Not a member\"", }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.NOT_A_MEMBER + return { state: "pending" } } }, "foo" @@ -75,7 +74,7 @@ test("It considers session invalid when membership status is \"Not a member\"", expect(isSessionValid).toBeFalsy() }) -test("It considers session invalid when membership status is \"Pending\"", async () => { +test("It considers session invalid when receiving HTTP 404, indicating user is not member of the organization", async () => { const sut = new GitHubOrganizationSessionValidator( { async graphql() { @@ -89,7 +88,7 @@ test("It considers session invalid when membership status is \"Pending\"", async }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.PENDING + throw { status: 404, message: "User is not member of organization"} } }, "foo" @@ -98,7 +97,7 @@ test("It considers session invalid when membership status is \"Pending\"", async expect(isSessionValid).toBeFalsy() }) -test("It considers session invalid when membership status is \"GitHub App Blocked\"", async () => { +test("It considers session invalid when receiving HTTP 404, indicating that the organization has blocked the GitHub app", async () => { const sut = new GitHubOrganizationSessionValidator( { async graphql() { @@ -112,7 +111,7 @@ test("It considers session invalid when membership status is \"GitHub App Blocke }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.GITHUB_APP_BLOCKED + throw { status: 403, message: "Organization has blocked GitHub app"} } }, "foo" @@ -121,7 +120,7 @@ test("It considers session invalid when membership status is \"GitHub App Blocke expect(isSessionValid).toBeFalsy() }) -test("It considers session invalid when membership status is \"Unknown\"", async () => { +test("It forwards error when getting membership status throws unknown error", async () => { const sut = new GitHubOrganizationSessionValidator( { async graphql() { @@ -135,11 +134,10 @@ test("It considers session invalid when membership status is \"Unknown\"", async }, async addCommentToPullRequest() {}, async getOrganizationMembershipStatus() { - return OrganizationMembershipStatus.UNKNOWN + throw { status: 500 } } }, "foo" ) - const isSessionValid = await sut.validateSession() - expect(isSessionValid).toBeFalsy() + await expect(sut.validateSession()).rejects.toEqual({ status: 500 }) }) \ No newline at end of file diff --git a/src/common/github/AccessTokenRefreshingGitHubClient.ts b/src/common/github/AccessTokenRefreshingGitHubClient.ts index 4d5ea36d..e5e4a9e7 100644 --- a/src/common/github/AccessTokenRefreshingGitHubClient.ts +++ b/src/common/github/AccessTokenRefreshingGitHubClient.ts @@ -5,10 +5,10 @@ import IGitHubClient, { GetRepositoryContentRequest, GetPullRequestCommentsRequest, GetOrganizationMembershipStatusRequest, + GetOrganizationMembershipStatusRequestResponse, AddCommentToPullRequestRequest, RepositoryContent, - PullRequestComment, - OrganizationMembershipStatus + PullRequestComment } from "./IGitHubClient" const HttpErrorSchema = z.object({ @@ -64,7 +64,7 @@ export default class AccessTokenRefreshingGitHubClient implements IGitHubClient async getOrganizationMembershipStatus( request: GetOrganizationMembershipStatusRequest - ): Promise { + ): Promise { return await this.send(async () => { return await this.gitHubClient.getOrganizationMembershipStatus(request) }) diff --git a/src/common/github/GitHubClient.ts b/src/common/github/GitHubClient.ts index dba5c281..cec899e9 100644 --- a/src/common/github/GitHubClient.ts +++ b/src/common/github/GitHubClient.ts @@ -7,9 +7,9 @@ import IGitHubClient, { GetPullRequestCommentsRequest, AddCommentToPullRequestRequest, GetOrganizationMembershipStatusRequest, + GetOrganizationMembershipStatusRequestResponse, RepositoryContent, - PullRequestComment, - OrganizationMembershipStatus + PullRequestComment } from "./IGitHubClient" type GitHubClientConfig = { @@ -95,33 +95,12 @@ export default class GitHubClient implements IGitHubClient { async getOrganizationMembershipStatus( request: GetOrganizationMembershipStatusRequest - ): Promise { + ): Promise { const accessToken = await this.accessTokenReader.getAccessToken() const octokit = new Octokit({ auth: accessToken }) - try { - const response = await octokit.rest.orgs.getMembershipForAuthenticatedUser({ - org: request.organizationName - }) - if (response.data.state == "active") { - return OrganizationMembershipStatus.ACTIVE - } else if (response.data.state == "pending") { - return OrganizationMembershipStatus.PENDING - } else { - return OrganizationMembershipStatus.UNKNOWN - } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ - } catch (error: any) { - if (error.status) { - if (error.status == 404) { - return OrganizationMembershipStatus.NOT_A_MEMBER - } else if (error.status == 403) { - return OrganizationMembershipStatus.GITHUB_APP_BLOCKED - } else { - throw error - } - } else { - throw error - } - } + const response = await octokit.rest.orgs.getMembershipForAuthenticatedUser({ + org: request.organizationName + }) + return { state: response.data.state } } } diff --git a/src/common/github/IGitHubClient.ts b/src/common/github/IGitHubClient.ts index a66e689f..1b349fc6 100644 --- a/src/common/github/IGitHubClient.ts +++ b/src/common/github/IGitHubClient.ts @@ -44,12 +44,8 @@ export type GetOrganizationMembershipStatusRequest = { readonly organizationName: string } -export enum OrganizationMembershipStatus { - NOT_A_MEMBER = "not_a_member", - ACTIVE = "active", - PENDING = "pending", - GITHUB_APP_BLOCKED = "github_app_blocked", - UNKNOWN = "unknown" +export type GetOrganizationMembershipStatusRequestResponse = { + readonly state: "active" | "pending" } export default interface IGitHubClient { @@ -57,5 +53,5 @@ export default interface IGitHubClient { getRepositoryContent(request: GetRepositoryContentRequest): Promise getPullRequestComments(request: GetPullRequestCommentsRequest): Promise addCommentToPullRequest(request: AddCommentToPullRequestRequest): Promise - getOrganizationMembershipStatus(request: GetOrganizationMembershipStatusRequest): Promise + getOrganizationMembershipStatus(request: GetOrganizationMembershipStatusRequest): Promise } diff --git a/src/common/session/GitHubOrganizationSessionValidator.ts b/src/common/session/GitHubOrganizationSessionValidator.ts index 837bc030..308ff043 100644 --- a/src/common/session/GitHubOrganizationSessionValidator.ts +++ b/src/common/session/GitHubOrganizationSessionValidator.ts @@ -1,4 +1,4 @@ -import IGitHubClient, { OrganizationMembershipStatus } from "../github/IGitHubClient" +import IGitHubClient from "../github/IGitHubClient" import ISessionValidator from "./ISessionValidator" export default class GitHubOrganizationSessionValidator implements ISessionValidator { @@ -11,9 +11,24 @@ export default class GitHubOrganizationSessionValidator implements ISessionValid } async validateSession(): Promise { - const status = await this.gitHubClient.getOrganizationMembershipStatus({ - organizationName: this.acceptedOrganization - }) - return status == OrganizationMembershipStatus.ACTIVE + try { + const response = await this.gitHubClient.getOrganizationMembershipStatus({ + organizationName: this.acceptedOrganization + }) + return response.state == "active" + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + } catch (error: any) { + if (error.status) { + if (error.status == 404) { + return false + } else if (error.status == 403) { + return false + } else { + throw error + } + } else { + throw error + } + } } }