diff --git a/app/octokit.server.ts b/app/octokit.server.ts index 0d763b49e..b57656124 100644 --- a/app/octokit.server.ts +++ b/app/octokit.server.ts @@ -8,7 +8,6 @@ import type { RequestParameters } from "@octokit/auth-app/dist-types/types"; import * as Sentry from "@sentry/remix"; import type { RequestError } from "@octokit/request-error"; import type { GraphqlResponseError } from "@octokit/graphql"; -import { GitHubAppAuthStrategy } from "./services/github-app-auth.server"; import { getInstallationForLogin } from "~/services/installation"; import { commitSession, getSession } from "./services/session.server"; import { catchError } from "./components"; @@ -26,7 +25,9 @@ export const redirectCookie = createCookie("redirect", { }); export async function getUser(request: Requests): Promise { - const res = await authenticator().isAuthenticated(toNodeRequest(request), {}); + const res = await ( + await authenticator() + ).isAuthenticated(toNodeRequest(request), {}); if (!res) { const set_cookie = await redirectCookie.serialize(request.url); throw redirect("/login", { @@ -118,26 +119,37 @@ export function getRootURL() { } } -export const gitHubStrategy = () => { - const callbackURL = `${getRootURL()}/auth/github/callback`; +export const gitHubStrategy = async () => { + const redirectURI = `${getRootURL()}/auth/github/callback`; + + const { GitHubAppAuthStrategy } = await import( + "./services/github-app-auth.server.mjs" + ); return new GitHubAppAuthStrategy( { - clientID: process.env.GITHUB_APP_CLIENT_ID!, + clientId: process.env.GITHUB_APP_CLIENT_ID!, clientSecret: process.env.GITHUB_APP_CLIENT_SECRET!, - callbackURL, - scope: ["user", "read:user"], + redirectURI, + scopes: ["user", "read:user"], }, - async ({ accessToken, profile, extraParams, refreshToken }) => { + async ({ + tokens: { + access_token, + refresh_token, + accessTokenExpiresIn, + refreshTokenExpiresIn, + }, + profile, + }) => { const installationId = await getInstallationForLogin(profile._json); - console.log({ extraParams }); return { login: profile._json.login, installationId, - accessToken, - refreshToken, - accessTokenExpiry: extraParams.accessTokenExpiresAt, - refreshTokenExpiry: extraParams.refreshTokenExpiresAt, + accessToken: access_token, + refreshToken: refresh_token, + accessTokenExpiry: accessTokenExpiresIn, + refreshTokenExpiry: refreshTokenExpiresIn, }; }, ); @@ -198,7 +210,7 @@ export async function call( export async function logoutAndRedirect(request: Request) { const res = await catchError( - authenticator().logout(request, { + (await authenticator()).logout(request, { redirectTo: "/", }), ); diff --git a/app/root.tsx b/app/root.tsx index c95fc98c7..e9655103f 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -41,7 +41,9 @@ export const loader: LoaderFunction = async ({ request }) => { ENV: { SENTRY_DSN: process.env.SENTRY_DSN, }, - user: _.pick(await authenticator().isAuthenticated(request), ["login"]), + user: _.pick(await (await authenticator()).isAuthenticated(request), [ + "login", + ]), colorMode: colorMode ?? "auto", }); }; diff --git a/app/routes/$owner/$repo/actions/$id/logs.tsx b/app/routes/$owner/$repo/actions/$id/logs.tsx index b08b13325..aa2c96923 100644 --- a/app/routes/$owner/$repo/actions/$id/logs.tsx +++ b/app/routes/$owner/$repo/actions/$id/logs.tsx @@ -10,6 +10,7 @@ import { getInstallationOctokit, getInstallationId, } from "~/services/installation"; +// @ts-expect-error import * as asc from "ansi-sequence-parser"; import { GearIcon } from "@primer/octicons-react"; import { @@ -137,7 +138,7 @@ export function ConstructLine({ line }: { line: string }) { case "command": case "endgroup": default: { - const spans = asc.parseAnsiSequences(line!); + const spans = asc.parseAnsiSequences(line!) as { value: string }[]; return ( <> diff --git a/app/routes/auth/github.tsx b/app/routes/auth/github.tsx index e03fa4599..93649d4c4 100644 --- a/app/routes/auth/github.tsx +++ b/app/routes/auth/github.tsx @@ -8,5 +8,5 @@ export async function loader() { } export const action: ActionFunction = async ({ request }) => { - return authenticator().authenticate("github", request); + return (await authenticator()).authenticate("github", request); }; diff --git a/app/routes/auth/github/callback.tsx b/app/routes/auth/github/callback.tsx index ea8168b13..70a3ba23a 100644 --- a/app/routes/auth/github/callback.tsx +++ b/app/routes/auth/github/callback.tsx @@ -20,7 +20,9 @@ export const loader: LoaderFunction = async ({ request }) => { } try { - await authenticator().authenticate("github", request, { + await ( + await authenticator() + ).authenticate("github", request, { successRedirect: await getRedirect(request), failureRedirect: "/login", }); diff --git a/app/routes/auth/logout.tsx b/app/routes/auth/logout.tsx index fe32aecbb..a1e4d8c43 100644 --- a/app/routes/auth/logout.tsx +++ b/app/routes/auth/logout.tsx @@ -2,6 +2,8 @@ import type { LoaderFunction } from "@remix-run/node"; import { authenticator } from "~/services/auth.server"; export const loader: LoaderFunction = async ({ request }) => - await authenticator().logout(request, { + await ( + await authenticator() + ).logout(request, { redirectTo: "/login", }); diff --git a/app/routes/debug.tsx b/app/routes/debug.tsx index 3e5543d3b..99f6349f9 100644 --- a/app/routes/debug.tsx +++ b/app/routes/debug.tsx @@ -4,7 +4,7 @@ import { getOctokit, getRootURL, getRedirect } from "~/octokit.server"; import { authenticator } from "~/services/auth.server"; import { splatObject } from "~/components/ErrorBoundary"; import * as Sentry from "@sentry/remix"; -import getCache from "~/services/cache"; +import { getCache } from "~/services/cache"; function pick(obj: T, keys: (keyof T)[]) { return _.pick(obj, keys); @@ -21,7 +21,7 @@ async function timeout(t: Promise) { export const loader = async ({ request }: LoaderFunctionArgs) => { const userObject = await timeout( - authenticator() + (await authenticator()) .isAuthenticated(request) .then((userObject) => userObject diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 196390c5c..4c82d98f7 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -11,7 +11,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const data = { error: session.get("error") }; // this will redirect if we're actually already logged in - await authenticator().isAuthenticated(request, { + await ( + await authenticator() + ).isAuthenticated(request, { successRedirect: "/", }); diff --git a/app/routes/storybook/cache.tsx b/app/routes/storybook/cache.tsx index 5c94adc58..67f58ef3a 100644 --- a/app/routes/storybook/cache.tsx +++ b/app/routes/storybook/cache.tsx @@ -1,4 +1,4 @@ -import getCache, { throwError } from "~/services/cache"; +import { getCache, throwError } from "~/services/cache"; import type { LoaderFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; diff --git a/app/services/auth.server.ts b/app/services/auth.server.ts index 3b932bfdd..489ca3256 100644 --- a/app/services/auth.server.ts +++ b/app/services/auth.server.ts @@ -18,8 +18,8 @@ export type SessionShape = Pick< // Create an instance of the authenticator, pass a generic with what // strategies will return and will store in the session -export let authenticator = _.memoize(() => { +export let authenticator = _.memoize(async () => { const authenticator = new Authenticator(sessionStorage); - authenticator.use(gitHubStrategy()); + authenticator.use(await gitHubStrategy()); return authenticator; }); diff --git a/app/services/cache.ts b/app/services/cache.ts index 0d29c7cf5..52534b75e 100644 --- a/app/services/cache.ts +++ b/app/services/cache.ts @@ -15,7 +15,7 @@ type XCache = StrategyOptions["cache"] & { const ONE_HOUR_IN_SECONDS = 60 * 60; -function getCache(): XCache { +function _getCache(): XCache { const { env } = process; const kv = createClient({ url: @@ -47,4 +47,4 @@ function getCache(): XCache { }; } -export default _.memoize(getCache); +export const getCache = _.memoize(_getCache); diff --git a/app/services/github-app-auth.server.ts b/app/services/github-app-auth.server.mts similarity index 96% rename from app/services/github-app-auth.server.ts rename to app/services/github-app-auth.server.mts index 9b2a3fd28..146766417 100644 --- a/app/services/github-app-auth.server.ts +++ b/app/services/github-app-auth.server.mts @@ -1,8 +1,8 @@ import type { GitHubExtraParams } from "remix-auth-github"; import { createAppAuth, createOAuthUserAuth } from "@octokit/auth-app"; -import { octokitFromConfig } from "~/octokit.server"; +import { octokitFromConfig } from "~/octokit.server.js"; import _ from "lodash"; -import getCache from "~/services/cache"; +import {getCache} from "~/services/cache.js"; import { GitHubStrategy } from "remix-auth-github"; function checkNonNull(name: string): NonNullable { diff --git a/app/services/installation.ts b/app/services/installation.ts index 287b0b6f4..ad5337635 100644 --- a/app/services/installation.ts +++ b/app/services/installation.ts @@ -1,11 +1,12 @@ import { json } from "@remix-run/node"; -import { getAppOctokit, getConfig } from "./github-app-auth.server"; import { getUser, octokitFromConfig } from "~/octokit.server"; import { createAppAuth } from "@octokit/auth-app"; export async function getInstallationOctokit(request: Request) { const installationId = await getInstallationId(request); + const { getConfig } = await import("./github-app-auth.server.mjs"); + const { auth, ...rest } = getConfig(); return octokitFromConfig({ authStrategy: createAppAuth, @@ -30,6 +31,8 @@ export async function getInstallationId( } export async function getInstallationForLogin(user: { login: string }) { + const { getAppOctokit } = await import("./github-app-auth.server.mjs"); + const appOctokit = await getAppOctokit(); const { data: installation } = await appOctokit.apps.getUserInstallation({ username: user.login, diff --git a/playwright.config.ts b/playwright.config.ts index cc30c5f59..5c6eb60f8 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -72,8 +72,10 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "yarn dev", + command: "yarn dev --port 3000", url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, + stdout: "pipe", + stderr: "pipe", }, }); diff --git a/test/auth.test.tsx b/test/auth.test.tsx index 9127bd1fe..49f82b72a 100644 --- a/test/auth.test.tsx +++ b/test/auth.test.tsx @@ -27,7 +27,10 @@ describe("auth", () => { it("logged in redirects", async () => { const session = await getSession(); - session.set(authenticator().sessionKey, { id: 1, login: "octocat" }); + session.set((await authenticator()).sessionKey, { + id: 1, + login: "octocat", + }); const request = new Request("http://localhost"); request.headers.set("Cookie", await commitSession(session)); diff --git a/test/cache.test.ts b/test/cache.test.ts index 2513d10e5..49f8868b1 100644 --- a/test/cache.test.ts +++ b/test/cache.test.ts @@ -1,4 +1,4 @@ -import getCache from "~/services/cache"; +import { getCache } from "~/services/cache"; import dotenv from "dotenv"; dotenv.config(); diff --git a/test/whatever.test.tsx b/test/whatever.test.tsx index 2022edd0b..6964f43a2 100644 --- a/test/whatever.test.tsx +++ b/test/whatever.test.tsx @@ -13,9 +13,10 @@ import { Octokit } from "@octokit/rest"; import { vitest } from "vitest"; test("hello", async () => { - let BasePage: React.FunctionComponent<{ asChildRoute: boolean }>; try { - BasePage = (await import("~/routes/index")).default; + var BasePage = await loadComponent<{ asChildRoute: boolean }>( + "~/routes/index.js", + ); } catch (e) { console.error(e); throw e; @@ -35,12 +36,12 @@ test("hello", async () => { await screen.findByText("Action Statuses"); }); -test('Displays error correctly', async () => { - let BasePage: React.FunctionComponent<{ asChildRoute: boolean }>; - let ErrorBoundary: React.FunctionComponent; +test("Displays error correctly", async () => { try { - BasePage = (await import("~/routes/index")).default; - ErrorBoundary = (await import("~/root")).ErrorBoundary; + var BasePage = await loadComponent<{ asChildRoute: boolean }>( + "~/routes/index.js", + ); + var ErrorBoundary = (await import("~/root.js")).ErrorBoundary; } catch (e) { console.error(e); throw e; @@ -54,13 +55,13 @@ test('Displays error correctly', async () => { }, path: "/", id: "root", - ErrorBoundary + ErrorBoundary, }, ]); await renderPrimer(); - expect((await screen.findByText("This is an error"))).toBeVisible(); + expect(await screen.findByText("This is an error")).toBeVisible(); }); test("RequestError instanceof", async () => { @@ -105,3 +106,7 @@ test("graphql", async () => { expect(error.mock.calls).toMatchSnapshot(); expect(warn.mock.calls).toMatchSnapshot(); }); + +async function loadComponent(path: string) { + return (await import(path)).default as unknown as React.FunctionComponent; +} diff --git a/tsconfig.json b/tsconfig.json index 295ec0428..7683c89d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { - "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx", "app/services/github-app-auth.server.mts"], "compilerOptions": { "lib": ["DOM", "DOM.Iterable", "ES2021"], "isolatedModules": true, "esModuleInterop": true, "jsx": "react-jsx", - "moduleResolution": "node", + "moduleResolution": "nodenext", "resolveJsonModule": true, "target": "ES2019", - "module": "ES2020", + "module": "NodeNext", "strict": true, "allowJs": true, "checkJs": true, @@ -18,7 +18,7 @@ "~/*": ["./app/*"] }, "skipLibCheck": true, - "types": ["jest", "node"], + "types": ["jest", "node", "@testing-library/jest-dom"], // Remix takes care of building everything in `remix build`. "noEmit": true diff --git a/yarn.lock b/yarn.lock index 05e1ce11d..46ae53d58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16083,31 +16083,27 @@ __metadata: linkType: hard "remix-auth-github@npm:^1.3.0": - version: 1.7.0 - resolution: "remix-auth-github@npm:1.7.0" - dependencies: - remix-auth-oauth2: "npm:^1.11.2" - peerDependencies: - "@remix-run/server-runtime": ^1.0.0 || ^2.0.0 - remix-auth: ^3.4.0 - checksum: 10/24744c36cec24284dcaeabe3af868f70049ae25e8d09f7954a6abf43b9b134ab473e0ed181764ca34b72545588dc1ff4e51513ab8ff0b820a3324b5183ec4a2a - languageName: node - linkType: hard - -"remix-auth-oauth2@npm:^1.11.2": - version: 1.11.2 - resolution: "remix-auth-oauth2@npm:1.11.2" + version: 1.8.0 + resolution: "remix-auth-github@npm:1.8.0" dependencies: - debug: "npm:^4.3.4" - uuid: "npm:^9.0.1" + remix-auth-oauth2: "npm:^2.2.0" peerDependencies: - "@remix-run/server-runtime": ^1.0.0 || ^2.0.0 + "@remix-run/cloudflare": ^1.0.0 || ^2.0.0 + "@remix-run/deno": ^1.0.0 || ^2.0.0 + "@remix-run/node": ^1.0.0 || ^2.0.0 remix-auth: ^3.6.0 - checksum: 10/e1bfcceb6b6126cfa74126e26693c98bf5e4c248213b971353a34ce4974bd2d6708b1053d3bd047d98506e88b64026622ca879ebc6bb7f0b85f531195bcf76a7 + peerDependenciesMeta: + "@remix-run/cloudflare": + optional: true + "@remix-run/deno": + optional: true + "@remix-run/node": + optional: true + checksum: 10/c473d43f6292d76b5f6f7d9860c35e1dba0ded1233c35304aae3d4e9639fd9fa481815729bf85c98e793820dae1b9968e2de45a6477ab9be18614fca7d9e34bc languageName: node linkType: hard -"remix-auth-oauth2@npm:^2.0.0": +"remix-auth-oauth2@npm:^2.0.0, remix-auth-oauth2@npm:^2.2.0": version: 2.4.0 resolution: "remix-auth-oauth2@npm:2.4.0" dependencies: @@ -18461,15 +18457,6 @@ __metadata: languageName: node linkType: hard -"uuid@npm:^9.0.1": - version: 9.0.1 - resolution: "uuid@npm:9.0.1" - bin: - uuid: dist/bin/uuid - checksum: 10/9d0b6adb72b736e36f2b1b53da0d559125ba3e39d913b6072f6f033e0c87835b414f0836b45bcfaf2bdf698f92297fea1c3cc19b0b258bc182c9c43cc0fab9f2 - languageName: node - linkType: hard - "uvu@npm:^0.5.0": version: 0.5.6 resolution: "uvu@npm:0.5.6"