Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: github public scope permissions #642

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/my-dashboard/_features/contributions/contributions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export function Contributions() {
search: debouncedSearch,
projectSlugs: projectSlug ? [projectSlug] : undefined,
types: ["ISSUE", "PULL_REQUEST"],
showLinkedIssues: false,
sort: "UPDATED_AT",
sortDirection: "DESC",
...filters,
Expand Down
39 changes: 21 additions & 18 deletions app/my-dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ActivitySection } from "@/app/my-dashboard/_sections/activity-section/a
import { AnimatedColumn } from "@/shared/components/animated-column-group/animated-column/animated-column";
import { withClientOnly } from "@/shared/components/client-only/client-only";
import { ScrollView } from "@/shared/components/scroll-view/scroll-view";
import { GithubPermissionsProvider } from "@/shared/features/github-permissions/github-permissions.context";
import { PageContent } from "@/shared/features/page-content/page-content";
import { PageWrapper } from "@/shared/features/page-wrapper/page-wrapper";
import { RequestPaymentFlowProvider } from "@/shared/panels/_flows/request-payment-flow/request-payment-flow.context";
Expand All @@ -30,24 +31,26 @@ function MyDashboardPage() {
],
}}
>
<RequestPaymentFlowProvider>
<PosthogCaptureOnMount eventName={"my_dashboard_viewed"} />

<AnimatedColumn className="h-full">
<ScrollView className="flex flex-col gap-md">
<PageContent classNames={{ base: "flex-none" }}>
<FinancialSection />
</PageContent>
<PageContent classNames={{ base: "tablet:overflow-hidden" }}>
<ActivitySection />
</PageContent>
</ScrollView>
</AnimatedColumn>

<ContributorSidepanel />
<ContributionsSidepanel />
<RewardDetailSidepanel />
</RequestPaymentFlowProvider>
<GithubPermissionsProvider>
<RequestPaymentFlowProvider>
<PosthogCaptureOnMount eventName={"my_dashboard_viewed"} />

<AnimatedColumn className="h-full">
<ScrollView className="flex flex-col gap-md">
<PageContent classNames={{ base: "flex-none" }}>
<FinancialSection />
</PageContent>
<PageContent classNames={{ base: "tablet:overflow-hidden" }}>
<ActivitySection />
</PageContent>
</ScrollView>
</AnimatedColumn>

<ContributorSidepanel />
<ContributionsSidepanel />
<RewardDetailSidepanel />
</RequestPaymentFlowProvider>
</GithubPermissionsProvider>
</PageWrapper>
);
}
Expand Down
12 changes: 8 additions & 4 deletions core/application/auth0-client-adapter/auth0-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { AppState, Auth0Provider as Provider } from "@auth0/auth0-react";
import { useRouter } from "next/navigation";
import { PropsWithChildren } from "react";

import { useLocalScopeStorage } from "@/core/application/auth0-client-adapter/hooks/use-local-scope-storage";

const domain = process.env.NEXT_PUBLIC_AUTH0_PROVIDER_DOMAIN;
const clientId = process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID;
const redirectUri = process.env.NEXT_PUBLIC_AUTH0_CALLBACK_URL;
Expand All @@ -12,15 +14,17 @@ const audience = process.env.NEXT_PUBLIC_AUTH0_AUDIENCE;

export function Auth0Provider({ children }: PropsWithChildren) {
const router = useRouter();
const [scopeStorage] = useLocalScopeStorage();

if (!(domain && clientId && redirectUri && audience)) {
return null;
}

const onRedirectCallback = (state: AppState | undefined) => {
function onRedirectCallback(state: AppState | undefined) {
if (state?.returnTo) {
router.push(state.returnTo);
}
};
}

return (
<Provider
Expand All @@ -30,10 +34,10 @@ export function Auth0Provider({ children }: PropsWithChildren) {
redirect_uri: redirectUri,
connection: connectionName,
audience,
// connection_scope: scopeStorage,
connection_scope: scopeStorage,
}}
cacheLocation="localstorage"
useRefreshTokens={true}
useRefreshTokens
onRedirectCallback={onRedirectCallback}
>
{children}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useLocalStorage } from "react-use";

export function useLocalScopeStorage() {
return useLocalStorage("dynamic-github-public-repo-scope");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useMemo } from "react";

import { Auth0ClientAdapter } from "@/core/application/auth0-client-adapter";
import { useLocalScopeStorage } from "@/core/application/auth0-client-adapter/hooks/use-local-scope-storage";
import { MeReactQueryAdapter } from "@/core/application/react-query-adapter/me";
import { useClientBootstrapContext } from "@/core/bootstrap/client-bootstrap-context";

import { useAuthUser } from "@/shared/hooks/auth/use-auth-user";

export function usePublicRepoScope() {
const [scopeStorage, setScopeStorage] = useLocalScopeStorage();

const {
clientBootstrap: { authProvider },
} = useClientBootstrapContext();
const { isAuthenticated = false, loginWithRedirect, loginWithPopup } = authProvider ?? {};

const { user, refetch } = useAuthUser();
const isAuthorized = useMemo(() => user?.isAuthorizedToApplyOnGithubIssues, [user]);

const { mutateAsync: logoutUser } = MeReactQueryAdapter.client.useLogoutMe({});

async function getPermissions() {
if (!scopeStorage) {
setScopeStorage(process.env.NEXT_PUBLIC_GITHUB_PUBLIC_REPO_SCOPE);
}

await logoutUser({});

if (loginWithPopup) await Auth0ClientAdapter.helpers.handleLoginWithPopup(loginWithPopup);

await refetch();
}

async function handleVerifyPermissions(onSuccess?: () => void) {
if (!isAuthenticated) {
if (loginWithRedirect) Auth0ClientAdapter.helpers.handleLoginWithRedirect(loginWithRedirect);
return;
}

if (!isAuthorized) {
await getPermissions();
onSuccess?.();
return;
}

onSuccess?.();
}

return { handleVerifyPermissions, getPermissions, isAuthorized };
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMutation } from "@tanstack/react-query";
import { useMutation, useQueryClient } from "@tanstack/react-query";

import {
UseMutationFacadeParams,
Expand All @@ -13,14 +13,20 @@ export function useDeleteApplication({
options,
}: UseMutationFacadeParams<ApplicationFacadePort["deleteApplication"], undefined, never, DeleteApplicationBody>) {
const applicationStoragePort = bootstrap.getApplicationStoragePortForClient();
const contributionStoragePort = bootstrap.getContributionStoragePortForClient();
const queryClient = useQueryClient();

return useMutation(
useMutationAdapter({
...applicationStoragePort.deleteApplication({ pathParams }),
options: {
...options,
onSuccess: async (data, variables, context) => {
// TODO invalidate application list query
// Invalidate application kanban list
await queryClient.invalidateQueries({
queryKey: contributionStoragePort.getContributions({}).tag,
exact: false,
});

options?.onSuccess?.(data, variables, context);
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"title": "Grant permissions",
"description": "We need your permission to write comments on your behalf and apply to selected issues directly on OnlyDust. Only need to be accepted once.",
"moreInfo": "Click on \"Grant Permissions\". A GitHub popup will appear. Click on \"Authorize OnlyDust\", and you'll be redirected back here to submit your application.",
"grantPermissions": "Write all public repository data"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { useState } from "react";

export function useGithubPublicScopePermissionModal() {
const [isOpen, setIsOpen] = useState(false);

return { isOpen, setIsOpen };
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import githubPermissionImage from "@/public/images/github/github-permission.png";
import { Github, SquareArrowOutUpRight } from "lucide-react";
import Image from "next/image";

import { Button } from "@/design-system/atoms/button/variants/button-default";
import { Typo } from "@/design-system/atoms/typo";
import { Modal } from "@/design-system/molecules/modal";

import { GithubPublicScopePermissionModalProps } from "./github-public-scope-permission-modal.types";

export function GithubPublicScopePermissionModal({
isOpen,
onOpenChange,
onRedirect,
}: GithubPublicScopePermissionModalProps) {
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
titleProps={{
translate: { token: "modals:githubPublicScopePermission.title" },
}}
footer={{
endContent: (
<Button
translate={{ token: "modals:githubPublicScopePermission.grantPermissions" }}
startIcon={{ component: Github }}
endIcon={{ component: SquareArrowOutUpRight }}
onClick={onRedirect}
/>
),
}}
size="xl"
background="gradient"
>
<div className="flex flex-col gap-lg">
<Image
src={githubPermissionImage}
alt="github permission"
className="h-full w-full object-cover object-center"
loading={"lazy"}
width={320}
height={50}
quality={100}
/>
<Typo size="xs" color="primary" translate={{ token: "modals:githubPublicScopePermission.description" }} />
<Typo size="xs" color="tertiary" translate={{ token: "modals:githubPublicScopePermission.moreInfo" }} />
</div>
</Modal>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ModalPort } from "@/design-system/molecules/modal";

export interface GithubPublicScopePermissionModalProps extends Pick<ModalPort<"div">, "isOpen" | "onOpenChange"> {
onRedirect: () => void;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import React, { PropsWithChildren, createContext, useContext, useEffect, useMemo, useState } from "react";

import { usePublicRepoScope } from "@/core/application/auth0-client-adapter/hooks/use-public-repo-scope";
import { GithubReactQueryAdapter } from "@/core/application/react-query-adapter/github";
import { ProjectReactQueryAdapter } from "@/core/application/react-query-adapter/project";

import { GithubPermissionModal } from "@/shared/features/github-permissions/_components/github-permission-modal/github-permission-modal";
import { useGithubPermissionModal } from "@/shared/features/github-permissions/_components/github-permission-modal/github-permission-modal.hooks";
import { GithubPublicScopePermissionModal } from "@/shared/features/github-permissions/_components/github-public-scope-permission-modal/github-public-scope-permission-modal";
import { useGithubPublicScopePermissionModal } from "@/shared/features/github-permissions/_components/github-public-scope-permission-modal/github-public-scope-permission-modal.hooks";
import { usePooling } from "@/shared/hooks/pooling/usePooling";

interface GithubPermissionsContextInterface {
Expand All @@ -14,6 +17,8 @@ interface GithubPermissionsContextInterface {
setIsGithubPermissionModalOpen: (isOpen: boolean) => void;
setEnablePooling: (enable: boolean) => void;
canCurrentUserUpdatePermissions: (repoId: number) => boolean;
isGithubPublicScopePermissionModalOpen: boolean;
setIsGithubPublicScopePermissionModalOpen: (isOpen: boolean) => void;
}

const GithubPermissionsContext = createContext<GithubPermissionsContextInterface>({
Expand All @@ -23,12 +28,17 @@ const GithubPermissionsContext = createContext<GithubPermissionsContextInterface
setIsGithubPermissionModalOpen: () => {},
setEnablePooling: () => {},
canCurrentUserUpdatePermissions: () => false,
isGithubPublicScopePermissionModalOpen: false,
setIsGithubPublicScopePermissionModalOpen: () => {},
});

export function GithubPermissionsProvider({ children, projectSlug }: PropsWithChildren & { projectSlug: string }) {
export function GithubPermissionsProvider({ children, projectSlug }: PropsWithChildren & { projectSlug?: string }) {
const [enablePooling, setEnablePooling] = useState(false);
const [repoId, setRepoId] = useState<number | undefined>();
const { isOpen: isGithubPermissionModalOpen, setIsOpen: setIsGithubPermissionModalOpen } = useGithubPermissionModal();
const { isOpen: isGithubPublicScopePermissionModalOpen, setIsOpen: setIsGithubPublicScopePermissionModalOpen } =
useGithubPublicScopePermissionModal();
const { handleVerifyPermissions } = usePublicRepoScope();

const { data: userOrganizations } = GithubReactQueryAdapter.client.useGetMyOrganizations({});

Expand Down Expand Up @@ -101,14 +111,29 @@ export function GithubPermissionsProvider({ children, projectSlug }: PropsWithCh
setIsGithubPermissionModalOpen,
setEnablePooling,
canCurrentUserUpdatePermissions,

isGithubPublicScopePermissionModalOpen,
setIsGithubPublicScopePermissionModalOpen,
}}
>
{children}

<GithubPermissionModal
onRedirect={handleRedirectToGithubFlow}
isOpen={isGithubPermissionModalOpen}
onOpenChange={setIsGithubPermissionModalOpen}
/>

<GithubPublicScopePermissionModal
onRedirect={() =>
handleVerifyPermissions(() => {
// Close the modal when the user has granted the permissions
setIsGithubPublicScopePermissionModalOpen(false);
})
}
isOpen={isGithubPublicScopePermissionModalOpen}
onOpenChange={setIsGithubPublicScopePermissionModalOpen}
/>
</GithubPermissionsContext.Provider>
);
}
Expand Down
2 changes: 2 additions & 0 deletions shared/modals/_translations/modals.translate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enGithubPermission from "@/shared/features/github-permissions/_components/github-permission-modal/_translations/github-permission.en.json";
import enGithubPublicScopePermission from "@/shared/features/github-permissions/_components/github-public-scope-permission-modal/_translations/github-public-scope-permission.en.json";
import enManageApplicants from "@/shared/modals/manage-applicants-modal/_translations/manage-applicants.en.json";
import enManageRewards from "@/shared/panels/_flows/reward-flow/modals/manage-rewards-modal/_translations/manage-rewards.en.json";

Expand All @@ -7,5 +8,6 @@ export const enModalsTranslation = {
manageApplicants: enManageApplicants,
manageRewards: enManageRewards,
githubPermission: enGithubPermission,
githubPublicScopePermission: enGithubPublicScopePermission,
},
};
Loading