Skip to content
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
29 changes: 1 addition & 28 deletions apps/dashboard/src/components/layouts/dashboard-layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { getRouteApi, Outlet } from "@tanstack/react-router";
import { lazy, Suspense, useEffect, useRef } from "react";
import { getGitHubAppAccessState } from "#/lib/github.functions";
import { lazy, Suspense } from "react";
import {
githubMyIssuesQueryOptions,
githubMyPullsQueryOptions,
} from "#/lib/github.query";
import { useShowOrgSetupQueryState } from "#/lib/github-access-dialog-query";
import { openGitHubAccessPrompt } from "#/lib/github-access-modal-store";
import { useGitHubRevalidation } from "#/lib/use-github-revalidation";
import { useHasMounted } from "#/lib/use-has-mounted";
import { DashboardBottomBar } from "./dashboard-bottombar";
Expand All @@ -31,16 +28,8 @@ export function DashboardLayout() {
const { user } = routeApi.useRouteContext();
const scope = { userId: user.id };
const hasMounted = useHasMounted();
const missingAppAuthPromptedRef = useRef(false);
const [showOrgSetup, setShowOrgSetup] = useShowOrgSetupQueryState();
useGitHubRevalidation(user.id);

const githubAccessQuery = useQuery({
queryKey: ["github-app-access-state", user.id],
queryFn: () => getGitHubAppAccessState(),
enabled: hasMounted,
staleTime: 5 * 60 * 1000,
});
const pullsQuery = useQuery({
...githubMyPullsQueryOptions(scope),
enabled: hasMounted,
Expand All @@ -65,22 +54,6 @@ export function DashboardLayout() {
: undefined;
const tabsReady = hasMounted && Boolean(pullsQuery.data && issuesQuery.data);

useEffect(() => {
if (
!hasMounted ||
showOrgSetup ||
missingAppAuthPromptedRef.current ||
!githubAccessQuery.data ||
githubAccessQuery.data.installationsAvailable
) {
return;
}

missingAppAuthPromptedRef.current = true;
openGitHubAccessPrompt({ source: "onboarding" });
void setShowOrgSetup(true);
}, [githubAccessQuery.data, hasMounted, setShowOrgSetup, showOrgSetup]);

return (
<div className="isolate flex h-dvh flex-col bg-muted">
<DashboardTopbar
Expand Down
31 changes: 0 additions & 31 deletions apps/dashboard/src/components/layouts/github-access-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
} from "@diffkit/ui/components/dialog";
import { cn } from "@diffkit/ui/lib/utils";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { getGitHubAppAccessState } from "#/lib/github.functions";
import {
findInstallationForOwner,
Expand All @@ -26,8 +25,6 @@ import {
} from "#/lib/github-access-modal-store";
import { useHasMounted } from "#/lib/use-has-mounted";

const ONBOARDING_STORAGE_KEY_PREFIX = "diffkit:github-access-onboarding:v1:";

function getExternalLinkProps(href: string) {
if (href.startsWith("http://") || href.startsWith("https://")) {
return { target: "_blank", rel: "noopener noreferrer" } as const;
Expand All @@ -36,35 +33,11 @@ function getExternalLinkProps(href: string) {
return {};
}

function getOnboardingStorageKey(userId: string) {
return `${ONBOARDING_STORAGE_KEY_PREFIX}${userId}`;
}

function dismissOnboarding(userId: string) {
window.localStorage.setItem(getOnboardingStorageKey(userId), "dismissed");
}

function isOnboardingDismissed(userId: string) {
return (
window.localStorage.getItem(getOnboardingStorageKey(userId)) === "dismissed"
);
}

export function GitHubAccessDialog({ userId }: { userId: string }) {
const hasMounted = useHasMounted();
const prompt = useGitHubAccessPrompt();
const [onboardingOpen, setOnboardingOpen] = useState(false);
const [showOrgSetup, setShowOrgSetup] = useShowOrgSetupQueryState();

useEffect(() => {
if (!hasMounted || isOnboardingDismissed(userId)) {
return;
}

setOnboardingOpen(true);
void setShowOrgSetup(true);
}, [hasMounted, setShowOrgSetup, userId]);

const isOpen = showOrgSetup;
const accessQuery = useQuery({
queryKey: ["github-app-access-state", userId],
Expand All @@ -88,10 +61,6 @@ export function GitHubAccessDialog({ userId }: { userId: string }) {

void setShowOrgSetup(false);
closeGitHubAccessPrompt();
if (onboardingOpen) {
dismissOnboarding(userId);
setOnboardingOpen(false);
}
}

const title = prompt?.repo
Expand Down
4 changes: 1 addition & 3 deletions apps/dashboard/src/lib/github-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export function buildGitHubAppInstallUrl(slug: string | null | undefined) {
return slug ? `https://github.com/apps/${slug}/installations/new` : null;
}

export function buildGitHubAppAuthorizePath(
returnTo = "/?show-org-setup=true",
) {
export function buildGitHubAppAuthorizePath(returnTo = "/setup") {
const params = new URLSearchParams({ returnTo });
return `/api/github/app/authorize?${params.toString()}`;
}
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/src/lib/github-app.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ export async function getGitHubOAuthAccountByUserId(userId: string) {
.get();
}

export async function hasGitHubAppUserAccount(userId: string) {
const row = await getGitHubAppUserAccountByUserId(userId);
return Boolean(row?.accessToken);
}

async function getGitHubAppUserAccountByUserId(userId: string) {
const db = getDb();

Expand Down
26 changes: 22 additions & 4 deletions apps/dashboard/src/lib/github.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,10 +742,15 @@ async function getGitHubContext(): Promise<GitHubContext | null> {
}

debug("github-access", "session found", { userId: session.user.id });
return {
session,
octokit: await getGitHubClientByUserId(session.user.id),
};
try {
return {
session,
octokit: await getGitHubClientByUserId(session.user.id),
};
} catch (error) {
console.error("[github-access] failed to create GitHub client", error);
return null;
}
});
}

Expand Down Expand Up @@ -2708,6 +2713,19 @@ export const getGitHubViewer = createServerFn({ method: "GET" }).handler(
},
);

export const checkSetupComplete = createServerFn({
method: "GET",
}).handler(async (): Promise<boolean> => {
const { getRequestSession } = await import("./auth-runtime");
const session = await getRequestSession();
if (!session) {
return false;
}

const { hasGitHubAppUserAccount } = await import("./github-app.server");
return hasGitHubAppUserAccount(session.user.id);
});

export const getGitHubAppAccessState = createServerFn({
method: "GET",
}).handler(async (): Promise<GitHubAppAccessState | null> => {
Expand Down
21 changes: 21 additions & 0 deletions apps/dashboard/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import { Route as rootRouteImport } from './routes/__root'
import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
import { Route as SetupRouteImport } from './routes/setup'
import { Route as RobotsDottxtRouteImport } from './routes/robots[.]txt'
import { Route as LoginRouteImport } from './routes/login'
import { Route as ProtectedRouteImport } from './routes/_protected'
Expand All @@ -30,6 +31,11 @@ const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
path: '/sitemap.xml',
getParentRoute: () => rootRouteImport,
} as any)
const SetupRoute = SetupRouteImport.update({
id: '/setup',
path: '/setup',
getParentRoute: () => rootRouteImport,
} as any)
const RobotsDottxtRoute = RobotsDottxtRouteImport.update({
id: '/robots.txt',
path: '/robots.txt',
Expand Down Expand Up @@ -107,6 +113,7 @@ export interface FileRoutesByFullPath {
'/': typeof ProtectedIndexRoute
'/login': typeof LoginRoute
'/robots.txt': typeof RobotsDottxtRoute
'/setup': typeof SetupRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/issues': typeof ProtectedIssuesRoute
'/pulls': typeof ProtectedPullsRoute
Expand All @@ -122,6 +129,7 @@ export interface FileRoutesByFullPath {
export interface FileRoutesByTo {
'/login': typeof LoginRoute
'/robots.txt': typeof RobotsDottxtRoute
'/setup': typeof SetupRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/issues': typeof ProtectedIssuesRoute
'/pulls': typeof ProtectedPullsRoute
Expand All @@ -140,6 +148,7 @@ export interface FileRoutesById {
'/_protected': typeof ProtectedRouteWithChildren
'/login': typeof LoginRoute
'/robots.txt': typeof RobotsDottxtRoute
'/setup': typeof SetupRoute
'/sitemap.xml': typeof SitemapDotxmlRoute
'/_protected/issues': typeof ProtectedIssuesRoute
'/_protected/pulls': typeof ProtectedPullsRoute
Expand All @@ -159,6 +168,7 @@ export interface FileRouteTypes {
| '/'
| '/login'
| '/robots.txt'
| '/setup'
| '/sitemap.xml'
| '/issues'
| '/pulls'
Expand All @@ -174,6 +184,7 @@ export interface FileRouteTypes {
to:
| '/login'
| '/robots.txt'
| '/setup'
| '/sitemap.xml'
| '/issues'
| '/pulls'
Expand All @@ -191,6 +202,7 @@ export interface FileRouteTypes {
| '/_protected'
| '/login'
| '/robots.txt'
| '/setup'
| '/sitemap.xml'
| '/_protected/issues'
| '/_protected/pulls'
Expand All @@ -209,6 +221,7 @@ export interface RootRouteChildren {
ProtectedRoute: typeof ProtectedRouteWithChildren
LoginRoute: typeof LoginRoute
RobotsDottxtRoute: typeof RobotsDottxtRoute
SetupRoute: typeof SetupRoute
SitemapDotxmlRoute: typeof SitemapDotxmlRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
ApiWebhooksGithubRoute: typeof ApiWebhooksGithubRoute
Expand All @@ -225,6 +238,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SitemapDotxmlRouteImport
parentRoute: typeof rootRouteImport
}
'/setup': {
id: '/setup'
path: '/setup'
fullPath: '/setup'
preLoaderRoute: typeof SetupRouteImport
parentRoute: typeof rootRouteImport
}
'/robots.txt': {
id: '/robots.txt'
path: '/robots.txt'
Expand Down Expand Up @@ -354,6 +374,7 @@ const rootRouteChildren: RootRouteChildren = {
ProtectedRoute: ProtectedRouteWithChildren,
LoginRoute: LoginRoute,
RobotsDottxtRoute: RobotsDottxtRoute,
SetupRoute: SetupRoute,
SitemapDotxmlRoute: SitemapDotxmlRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
ApiWebhooksGithubRoute: ApiWebhooksGithubRoute,
Expand Down
7 changes: 7 additions & 0 deletions apps/dashboard/src/routes/_protected.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createFileRoute, redirect } from "@tanstack/react-router";
import { DashboardLayout } from "#/components/layouts/dashboard-layout";
import { ErrorScreen } from "#/components/layouts/error-screen";
import { getSession } from "#/lib/auth.functions";
import { checkSetupComplete } from "#/lib/github.functions";
import { buildSeo, formatPageTitle, PRIVATE_ROUTE_HEADERS } from "#/lib/seo";

export const Route = createFileRoute("/_protected")({
Expand All @@ -13,6 +14,12 @@ export const Route = createFileRoute("/_protected")({
search: { redirect: location.href },
});
}

const setupComplete = await checkSetupComplete();
if (!setupComplete) {
throw redirect({ to: "/setup" });
}

return { user: session.user, session: session.session };
},
headers: () => PRIVATE_ROUTE_HEADERS,
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/routes/api/github/app/authorize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PRIVATE_ROUTE_HEADERS } from "#/lib/seo";

const STATE_COOKIE = "github_app_oauth_state";
const RETURN_TO_COOKIE = "github_app_oauth_return_to";
const DEFAULT_RETURN_TO = "/?show-org-setup=true";
const DEFAULT_RETURN_TO = "/setup";

function normalizeReturnTo(value: string | null) {
if (!value || !value.startsWith("/") || value.startsWith("//")) {
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/routes/api/github/app/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { PRIVATE_ROUTE_HEADERS } from "#/lib/seo";

const STATE_COOKIE = "github_app_oauth_state";
const RETURN_TO_COOKIE = "github_app_oauth_return_to";
const DEFAULT_RETURN_TO = "/?show-org-setup=true";
const DEFAULT_RETURN_TO = "/setup";

function getCookie(request: Request, name: string) {
const cookieHeader = request.headers.get("cookie") ?? "";
Expand Down
Loading
Loading