From 86d4b0f2ababfc64fb1ca877cfaab53e21696c0a Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 2 Jan 2025 17:41:20 +0200 Subject: [PATCH 1/2] Reload refresh token if required --- apps/web/app/(app)/PermissionsCheck.tsx | 1 + .../app/(app)/permissions/consent/page.tsx | 38 +++++++++++++++++++ apps/web/app/(app)/permissions/error/page.tsx | 7 +++- apps/web/app/(landing)/login/LoginForm.tsx | 19 ++++++---- apps/web/app/(landing)/login/page.tsx | 2 +- apps/web/app/api/auth/[...nextauth]/auth.ts | 20 +++++++++- apps/web/components/TokenCheck.tsx | 6 ++- apps/web/utils/actions/permissions.ts | 17 ++++++++- apps/web/utils/auth.ts | 24 ++++++++---- 9 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 apps/web/app/(app)/permissions/consent/page.tsx diff --git a/apps/web/app/(app)/PermissionsCheck.tsx b/apps/web/app/(app)/PermissionsCheck.tsx index 3d7dce79b..d7e60b8fc 100644 --- a/apps/web/app/(app)/PermissionsCheck.tsx +++ b/apps/web/app/(app)/PermissionsCheck.tsx @@ -15,6 +15,7 @@ export function PermissionsCheck() { checkPermissionsAction().then((result) => { if (!result?.hasAllPermissions) router.replace("/permissions/error"); + if (!result?.hasRefreshToken) router.replace("/permissions/consent"); }); }, [router]); diff --git a/apps/web/app/(app)/permissions/consent/page.tsx b/apps/web/app/(app)/permissions/consent/page.tsx new file mode 100644 index 000000000..02f1d9138 --- /dev/null +++ b/apps/web/app/(app)/permissions/consent/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { logOut } from "@/utils/user"; +import { PageHeading, TypographyP } from "@/components/Typography"; + +export default function PermissionsErrorPage() { + return ( +
+ + We are missing consent 😔 + + + + You must sign in and give access to all permissions for Inbox Zero to + work. + + + + +
+ +
+
+ ); +} diff --git a/apps/web/app/(app)/permissions/error/page.tsx b/apps/web/app/(app)/permissions/error/page.tsx index 69db18c7d..1533164a7 100644 --- a/apps/web/app/(app)/permissions/error/page.tsx +++ b/apps/web/app/(app)/permissions/error/page.tsx @@ -9,7 +9,7 @@ export default function PermissionsErrorPage() { return (
- You are missing permissions 😔 + We are missing permissions 😔 @@ -17,7 +17,10 @@ export default function PermissionsErrorPage() { work. - diff --git a/apps/web/app/(landing)/login/LoginForm.tsx b/apps/web/app/(landing)/login/LoginForm.tsx index ffd0f6701..a58fd5e50 100644 --- a/apps/web/app/(landing)/login/LoginForm.tsx +++ b/apps/web/app/(landing)/login/LoginForm.tsx @@ -1,12 +1,12 @@ "use client"; +import { useState } from "react"; import { signIn } from "next-auth/react"; import Image from "next/image"; import { useSearchParams } from "next/navigation"; import { Button } from "@/components/Button"; import { Modal, useModal } from "@/components/Modal"; import { SectionDescription } from "@/components/Typography"; -import { useState } from "react"; export function LoginForm() { const searchParams = useSearchParams(); @@ -53,12 +53,17 @@ export function LoginForm() { loading={loading} onClick={() => { setLoading(true); - signIn("google", { - consent: error === "RefreshAccessTokenError", - ...(next && next.length > 0 - ? { callbackUrl: next } - : { callbackUrl: "/welcome" }), - }); + signIn( + "google", + { + ...(next && next.length > 0 + ? { callbackUrl: next } + : { callbackUrl: "/welcome" }), + }, + error === "RequiresReconsent" + ? { consent: true } + : undefined, + ); }} > I agree diff --git a/apps/web/app/(landing)/login/page.tsx b/apps/web/app/(landing)/login/page.tsx index e992b4d06..b63ccb495 100644 --- a/apps/web/app/(landing)/login/page.tsx +++ b/apps/web/app/(landing)/login/page.tsx @@ -41,7 +41,7 @@ export default async function AuthenticationPage({
- {searchParams?.error && ( + {searchParams?.error && searchParams?.error !== "RequiresReconsent" && ( <> { + if (req?.url) { + const url = new URL(req?.url); + const consent = url.searchParams.get("consent"); + if (consent) { + logger.info("Consent requested"); + return getAuthOptions({ consent: true }); + } + } + + return authOptions; +}); diff --git a/apps/web/components/TokenCheck.tsx b/apps/web/components/TokenCheck.tsx index f4d663407..9e1ff7721 100644 --- a/apps/web/components/TokenCheck.tsx +++ b/apps/web/components/TokenCheck.tsx @@ -10,8 +10,12 @@ export function TokenCheck() { useEffect(() => { if (session?.error === "RefreshAccessTokenError") { - console.log("Token check error"); router.replace("/login?error=RefreshAccessTokenError"); + return; + } + if (session?.error === "RequiresReconsent") { + router.replace("/login?error=RequiresReconsent"); + return; } }, [session, router]); diff --git a/apps/web/utils/actions/permissions.ts b/apps/web/utils/actions/permissions.ts index fc83dfe69..c84ad714d 100644 --- a/apps/web/utils/actions/permissions.ts +++ b/apps/web/utils/actions/permissions.ts @@ -21,7 +21,22 @@ export const checkPermissionsAction = withActionInstrumentation( token.token, ); if (error) return { error }; - return { hasAllPermissions }; + + if (!hasAllPermissions) return { hasAllPermissions: false }; + + // Check for refresh token + const user = await prisma.account.findFirst({ + where: { + userId: session.user.id, + provider: "google", + }, + select: { refresh_token: true }, + }); + + if (!user?.refresh_token) + return { hasRefreshToken: false, hasAllPermissions }; + + return { hasRefreshToken: true, hasAllPermissions }; } catch (error) { return { error: "Failed to check permissions" }; } diff --git a/apps/web/utils/auth.ts b/apps/web/utils/auth.ts index dee6855e9..9e38b871c 100644 --- a/apps/web/utils/auth.ts +++ b/apps/web/utils/auth.ts @@ -23,9 +23,9 @@ export const SCOPES = [ : []), ]; -const getAuthOptions: (options?: { consent: boolean }) => NextAuthConfig = ( - options, -) => ({ +export const getAuthOptions: (options?: { + consent: boolean; +}) => NextAuthConfig = (options) => ({ // debug: true, providers: [ GoogleProvider({ @@ -45,7 +45,7 @@ const getAuthOptions: (options?: { consent: boolean }) => NextAuthConfig = ( }, }), ], - adapter: PrismaAdapter(prisma) as any, // TODO + adapter: PrismaAdapter(prisma), session: { strategy: "jwt" }, // based on: https://authjs.dev/guides/basics/refresh-token-rotation // and: https://github.com/nextauthjs/next-auth-refresh-token-example/blob/main/pages/api/auth/%5B...nextauth%5D.js @@ -158,8 +158,6 @@ const getAuthOptions: (options?: { consent: boolean }) => NextAuthConfig = ( }, }); -export const authOptions = getAuthOptions(); - /** * Takes a token, and returns a new token with updated * `access_token` and `expires_at`. If an error occurs, @@ -262,11 +260,23 @@ export async function saveRefreshToken( }, account: Pick, ) { + const refreshToken = tokens.refresh_token ?? account.refresh_token; + + if (!refreshToken) { + logger.error("Attempted to save null refresh token", { + providerAccountId: account.providerAccountId, + }); + captureException("Cannot save null refresh token", { + extra: { providerAccountId: account.providerAccountId }, + }); + return; + } + return await prisma.account.update({ data: { access_token: tokens.access_token, expires_at: tokens.expires_at, - refresh_token: tokens.refresh_token ?? account.refresh_token, + refresh_token: refreshToken, }, where: { provider_providerAccountId: { From d8408bc106932bd3ba4967d8990e57d5212ba7c5 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Thu, 2 Jan 2025 19:30:18 +0200 Subject: [PATCH 2/2] Fix coderabbit nitpick comments --- apps/web/app/(app)/permissions/consent/page.tsx | 2 +- apps/web/app/api/auth/[...nextauth]/auth.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(app)/permissions/consent/page.tsx b/apps/web/app/(app)/permissions/consent/page.tsx index 02f1d9138..097a76d90 100644 --- a/apps/web/app/(app)/permissions/consent/page.tsx +++ b/apps/web/app/(app)/permissions/consent/page.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { logOut } from "@/utils/user"; import { PageHeading, TypographyP } from "@/components/Typography"; -export default function PermissionsErrorPage() { +export default function PermissionsConsentPage() { return (
diff --git a/apps/web/app/api/auth/[...nextauth]/auth.ts b/apps/web/app/api/auth/[...nextauth]/auth.ts index e80aaaf1f..fc9655592 100644 --- a/apps/web/app/api/auth/[...nextauth]/auth.ts +++ b/apps/web/app/api/auth/[...nextauth]/auth.ts @@ -4,7 +4,7 @@ import { createScopedLogger } from "@/utils/logger"; const logger = createScopedLogger("Auth API"); -const authOptions = getAuthOptions(); +const defaultAuthOptions = getAuthOptions(); export const { handlers: { GET, POST }, @@ -19,5 +19,5 @@ export const { } } - return authOptions; + return defaultAuthOptions; });