Skip to content

Commit

Permalink
fix: fix import & link project from GitHub light app
Browse files Browse the repository at this point in the history
If the user has no access to the project himself, he cannot actually do it.
  • Loading branch information
gregberge committed Aug 27, 2024
1 parent 0ca50e0 commit 0c59fec
Show file tree
Hide file tree
Showing 9 changed files with 114 additions and 28 deletions.
8 changes: 8 additions & 0 deletions apps/backend/src/graphql/__generated__/resolver-types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/backend/src/graphql/__generated__/schema.gql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 56 additions & 5 deletions apps/backend/src/graphql/definitions/Project.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { assertNever } from "@argos/util/assertNever";
import { invariant } from "@argos/util/invariant";
import { captureException } from "@sentry/node";
import gqlTag from "graphql-tag";
Expand All @@ -17,6 +18,7 @@ import {
resolveProjectName,
} from "@/database/services/project.js";
import { notifyDiscord } from "@/discord/index.js";
import { getInstallationOctokit, getTokenOctokit } from "@/github/client.js";
import { formatGlProject, getGitlabClientFromAccount } from "@/gitlab/index.js";
import { getOrCreateGithubRepository } from "@/graphql/services/github.js";

Expand All @@ -38,6 +40,11 @@ export const typeDefs = gql`
auto
}
enum GitHubAppType {
main
light
}
type GithubRepository implements Node {
id: ID!
defaultBranch: String!
Expand Down Expand Up @@ -131,6 +138,7 @@ export const typeDefs = gql`
repo: String!
owner: String!
accountSlug: String!
app: GitHubAppType!
}
input ImportGitlabProjectInput {
Expand All @@ -156,6 +164,7 @@ export const typeDefs = gql`
projectId: ID!
repo: String!
owner: String!
app: GitHubAppType!
}
input UnlinkGithubRepositoryInput {
Expand Down Expand Up @@ -253,22 +262,52 @@ ${input.account.slug} / ${input.project.name}
});
}

const importGithubProject = async (props: {
async function getGitHubOctokitFromAppType(input: {
account: Account;
creator: User;
app: "main" | "light";
}) {
switch (input.app) {
case "main":
return getTokenOctokit(input.creator.accessToken);
case "light":
invariant(
input.account.githubLightInstallationId,
"GitHub Light installation not found",
);
return getInstallationOctokit(input.account.githubLightInstallationId);
default:
assertNever(input.app);
}
}

async function importGithubProject(props: {
accountSlug: string;
creator: User;
repo: string;
owner: string;
}) => {
app: "main" | "light";
}) {
const account = await Account.query()
.findOne({ slug: props.accountSlug })
.throwIfNotFound();

const permissions = await account.$getPermissions(props.creator);

if (!permissions.includes("admin")) {
throw forbidden();
}

const octokit = await getGitHubOctokitFromAppType({
account,
creator: props.creator,
app: props.app,
});

invariant(octokit, "Octokit not found");

const ghRepo = await getOrCreateGithubRepository({
accessToken: props.creator.accessToken,
octokit,
repo: props.repo,
owner: props.owner,
});
Expand All @@ -292,7 +331,7 @@ const importGithubProject = async (props: {
});

return project;
};
}

const getOrCreateGitlabProject = async (props: {
account: Account;
Expand Down Expand Up @@ -548,6 +587,7 @@ export const resolvers: IResolvers = {
repo: args.input.repo,
owner: args.input.owner,
creator: ctx.auth.user,
app: args.input.app,
});
},
importGitlabProject: async (_root, args, ctx) => {
Expand Down Expand Up @@ -598,10 +638,21 @@ export const resolvers: IResolvers = {
const project = await getAdminProject({
id: args.input.projectId,
user: ctx.auth.user,
withGraphFetched: "account",
});

invariant(project.account, "account not fetched");

const octokit = await getGitHubOctokitFromAppType({
account: project.account,
creator: ctx.auth.user,
app: args.input.app,
});

invariant(octokit, "Octokit not found");

const ghRepo = await getOrCreateGithubRepository({
accessToken: ctx.auth.user.accessToken,
octokit,
owner: args.input.owner,
repo: args.input.repo,
});
Expand Down
5 changes: 2 additions & 3 deletions apps/backend/src/graphql/services/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,11 @@ export async function getOrCreateGithubAccount(
}

export async function getOrCreateGithubRepository(args: {
accessToken: string;
octokit: Octokit;
repo: string;
owner: string;
}): Promise<GithubRepository> {
const octokit = getTokenOctokit(args.accessToken);
const ghApiRepo = await octokit.repos
const ghApiRepo = await args.octokit.repos
.get({
owner: args.owner,
repo: args.repo,
Expand Down
23 changes: 13 additions & 10 deletions apps/frontend/src/containers/Project/ConnectRepository.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { GithubInstallationsSelect } from "@/containers/GithubInstallationsSelec
import { GithubRepositoryList } from "@/containers/GithubRepositoryList";
import { GitlabNamespacesSelect } from "@/containers/GitlabNamespacesSelect";
import { DocumentType, graphql } from "@/gql";
import { AccountPermission } from "@/gql/graphql";
import { AccountPermission, GitHubAppType } from "@/gql/graphql";
import {
Button,
ButtonIcon,
Expand Down Expand Up @@ -84,15 +84,18 @@ type GitlabNamespace = NonNullable<

type GithubInstallationsProps = {
installations: GithubInstallation[];
onSelectRepository: (repo: {
name: string;
owner_login: string;
id: string;
}) => void;
onSelectRepository: (
repo: {
name: string;
owner_login: string;
id: string;
},
app: GitHubAppType,
) => void;
disabled?: boolean;
connectButtonLabel: string;
onSwitch: () => void;
app: "main" | "light";
app: GitHubAppType;
};

function GithubInstallations(props: GithubInstallationsProps) {
Expand All @@ -112,7 +115,7 @@ function GithubInstallations(props: GithubInstallationsProps) {
<GithubRepositoryList
installationId={value}
disabled={props.disabled}
onSelectRepository={props.onSelectRepository}
onSelectRepository={(repo) => props.onSelectRepository(repo, props.app)}
connectButtonLabel={props.connectButtonLabel}
app={props.app}
/>
Expand Down Expand Up @@ -327,7 +330,7 @@ export const ConnectRepository = (props: ConnectRepositoryProps) => {
disabled={props.disabled}
connectButtonLabel={buttonLabels[props.variant]}
onSwitch={() => setAndStoreProvider(null)}
app="main"
app={GitHubAppType.Main}
/>
);
}
Expand All @@ -342,7 +345,7 @@ export const ConnectRepository = (props: ConnectRepositoryProps) => {
disabled={props.disabled}
connectButtonLabel={buttonLabels[props.variant]}
onSwitch={() => setAndStoreProvider(null)}
app="light"
app={GitHubAppType.Light}
/>
);
}
Expand Down
6 changes: 4 additions & 2 deletions apps/frontend/src/containers/Project/GitRepository.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ const LinkGithubRepositoryMutation = graphql(`
$projectId: ID!
$repo: String!
$owner: String!
$app: GitHubAppType!
) {
linkGithubRepository(
input: { projectId: $projectId, repo: $repo, owner: $owner }
input: { projectId: $projectId, repo: $repo, owner: $owner, app: $app }
) {
id
...ProjectGitRepository_Project
Expand Down Expand Up @@ -174,12 +175,13 @@ const LinkRepository = (props: { projectId: string; accountSlug: string }) => {
variant="link"
accountSlug={props.accountSlug}
disabled={loading}
onSelectRepository={(repo) => {
onSelectRepository={(repo, app) => {
linkGithubRepository({
variables: {
projectId: props.projectId,
repo: repo.name,
owner: repo.owner_login,
app,
},
});
}}
Expand Down
8 changes: 4 additions & 4 deletions apps/frontend/src/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const documents = {
"\n mutation DeleteProjectMutation($projectId: ID!) {\n deleteProject(id: $projectId)\n }\n": types.DeleteProjectMutationDocument,
"\n fragment ProjectDelete_Project on Project {\n id\n name\n account {\n id\n slug\n }\n }\n": types.ProjectDelete_ProjectFragmentDoc,
"\n fragment ProjectGitRepository_Project on Project {\n id\n account {\n id\n slug\n }\n repository {\n __typename\n id\n fullName\n url\n }\n prCommentEnabled\n }\n": types.ProjectGitRepository_ProjectFragmentDoc,
"\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n": types.ProjectGitRepository_LinkGithubRepositoryDocument,
"\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n $app: GitHubAppType!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner, app: $app }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n": types.ProjectGitRepository_LinkGithubRepositoryDocument,
"\n mutation ProjectGitRepository_unlinkGithubRepository($projectId: ID!) {\n unlinkGithubRepository(input: { projectId: $projectId }) {\n id\n ...ProjectGitRepository_Project\n }\n }\n": types.ProjectGitRepository_UnlinkGithubRepositoryDocument,
"\n mutation ProjectGitRepository_linkGitlabProject(\n $projectId: ID!\n $gitlabProjectId: ID!\n ) {\n linkGitlabProject(\n input: { projectId: $projectId, gitlabProjectId: $gitlabProjectId }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n": types.ProjectGitRepository_LinkGitlabProjectDocument,
"\n mutation ProjectGitRepository_unlinkGitlabProject($projectId: ID!) {\n unlinkGitlabProject(input: { projectId: $projectId }) {\n id\n ...ProjectGitRepository_Project\n }\n }\n": types.ProjectGitRepository_UnlinkGitlabProjectDocument,
Expand Down Expand Up @@ -100,7 +100,7 @@ const documents = {
"\n mutation AccountSlack_UninstallSlack($accountId: ID!) {\n uninstallSlack(input: { accountId: $accountId }) {\n id\n ...TeamSlack_Account\n }\n }\n": types.AccountSlack_UninstallSlackDocument,
"\n query UpgradeDialog_me {\n me {\n id\n slug\n hasSubscribedToTrial\n ...AccountItem_Account\n teams {\n id\n slug\n subscriptionStatus\n ...AccountItem_Account\n }\n }\n }\n": types.UpgradeDialog_MeDocument,
"\n fragment UserListRow_user on User {\n id\n slug\n name\n avatar {\n ...AccountAvatarFragment\n }\n }\n": types.UserListRow_UserFragmentDoc,
"\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n ) {\n importGithubProject(\n input: { repo: $repo, owner: $owner, accountSlug: $accountSlug }\n ) {\n id\n slug\n }\n }\n": types.NewProject_ImportGithubProjectDocument,
"\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n $app: GitHubAppType!\n ) {\n importGithubProject(\n input: {\n repo: $repo\n owner: $owner\n accountSlug: $accountSlug\n app: $app\n }\n ) {\n id\n slug\n }\n }\n": types.NewProject_ImportGithubProjectDocument,
"\n mutation NewProject_importGitlabProject(\n $gitlabProjectId: ID!\n $accountSlug: String!\n ) {\n importGitlabProject(\n input: { gitlabProjectId: $gitlabProjectId, accountSlug: $accountSlug }\n ) {\n id\n slug\n }\n }\n": types.NewProject_ImportGitlabProjectDocument,
"\n query AccountProjects_account($slug: String!) {\n account(slug: $slug) {\n id\n permissions\n projects(first: 100, after: 0) {\n edges {\n id\n ...ProjectList_Project\n }\n }\n }\n }\n": types.AccountProjects_AccountDocument,
"\n query AccountSettings_account($slug: String!) {\n account(slug: $slug) {\n id\n\n ... on Team {\n plan {\n id\n fineGrainedAccessControlIncluded\n }\n }\n\n ...TeamSlack_Account\n ...TeamMembers_Team\n ...TeamDelete_Team\n ...AccountChangeName_Account\n ...AccountChangeSlug_Account\n ...PlanCard_Account\n ...AccountGitLab_Account\n ...TeamGitHubSSO_Team\n ...TeamAccessRole_Team\n ...TeamGitHubLight_Team\n }\n }\n": types.AccountSettings_AccountDocument,
Expand Down Expand Up @@ -316,7 +316,7 @@ export function graphql(source: "\n fragment ProjectGitRepository_Project on Pr
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n"): (typeof documents)["\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n"];
export function graphql(source: "\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n $app: GitHubAppType!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner, app: $app }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n"): (typeof documents)["\n mutation ProjectGitRepository_linkGithubRepository(\n $projectId: ID!\n $repo: String!\n $owner: String!\n $app: GitHubAppType!\n ) {\n linkGithubRepository(\n input: { projectId: $projectId, repo: $repo, owner: $owner, app: $app }\n ) {\n id\n ...ProjectGitRepository_Project\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down Expand Up @@ -496,7 +496,7 @@ export function graphql(source: "\n fragment UserListRow_user on User {\n id
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n ) {\n importGithubProject(\n input: { repo: $repo, owner: $owner, accountSlug: $accountSlug }\n ) {\n id\n slug\n }\n }\n"): (typeof documents)["\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n ) {\n importGithubProject(\n input: { repo: $repo, owner: $owner, accountSlug: $accountSlug }\n ) {\n id\n slug\n }\n }\n"];
export function graphql(source: "\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n $app: GitHubAppType!\n ) {\n importGithubProject(\n input: {\n repo: $repo\n owner: $owner\n accountSlug: $accountSlug\n app: $app\n }\n ) {\n id\n slug\n }\n }\n"): (typeof documents)["\n mutation NewProject_importGithubProject(\n $repo: String!\n $owner: String!\n $accountSlug: String!\n $app: GitHubAppType!\n ) {\n importGithubProject(\n input: {\n repo: $repo\n owner: $owner\n accountSlug: $accountSlug\n app: $app\n }\n ) {\n id\n slug\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading

0 comments on commit 0c59fec

Please sign in to comment.