Handle missing OAuth scopes gracefully with error messaging

Implemented error handling for missing OAuth scopes, displaying user-friendly messages in the login flow. Enhanced TRPC middleware and client logic to trigger logout and redirect users to a dedicated error state. Refactored error indicator components to ensure UI consistency.
This commit is contained in:
Dak Washbrook
2025-05-08 22:34:38 -07:00
parent 16f620214c
commit d1b22fd673
7 changed files with 59 additions and 32 deletions

View File

@@ -0,0 +1,26 @@
'use client';
import { TriangleAlert } from 'lucide-react';
import { useQueryState } from 'nuqs';
const errorMessages: Record<string, string> = {
'required-scopes-missing':
'Were missing the permissions needed to craft your full experience. Please sign in again and allow the requested access.',
};
const ErrorMessage = () => {
const [error] = useQueryState('error');
if (!error || !(error in errorMessages)) return null;
return (
<div className="border-red/10 bg-red/5 min-w-0 max-w-fit shrink overflow-hidden break-words rounded-lg border p-4 dark:border-white/10 dark:bg-white/5">
<div className="flex items-center">
<TriangleAlert size={28} />
<p className="ml-2 text-sm text-black/80 dark:text-white/80">{errorMessages[error]}</p>
</div>
</div>
);
};
export default ErrorMessage;

View File

@@ -3,9 +3,11 @@
import { useEffect, type ReactNode, useState, Suspense } from 'react';
import type { EnvVarInfo } from '@zero/server/auth-providers';
import { useRouter, useSearchParams } from 'next/navigation';
import ErrorMessage from '@/app/(auth)/login/error-message';
import { signIn, useSession } from '@/lib/auth-client';
import { Google } from '@/components/icons/icons';
import { Button } from '@/components/ui/button';
import { TriangleAlert } from 'lucide-react';
import Image from 'next/image';
import { toast } from 'sonner';
import Link from 'next/link';
@@ -157,20 +159,7 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
<div className="rounded-lg border border-black/10 bg-black/5 p-5 dark:border-white/10 dark:bg-white/5">
<div className="flex flex-col space-y-4">
<div className="flex items-center">
<svg
className="h-5 w-5 text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<TriangleAlert size={28} />
<h3 className="ml-2 text-base font-medium text-black dark:text-white">
Configuration Required
</h3>
@@ -295,20 +284,7 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
{shouldShowSimplifiedMessage && (
<div className="rounded-lg border border-black/10 bg-black/5 p-4 dark:border-white/10 dark:bg-white/5">
<div className="flex items-center">
<svg
className="h-5 w-5 text-black dark:text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<TriangleAlert size={28} />
<p className="ml-2 text-sm text-black/80 dark:text-white/80">
Authentication service unavailable
</p>
@@ -316,6 +292,8 @@ function LoginClientContent({ providers, isProd }: LoginClientProps) {
</div>
)}
<ErrorMessage />
{!hasMissingRequiredProviders && (
<div className="relative z-10 mx-auto flex w-full flex-col items-center justify-center gap-2">
{sortedProviders.map(

View File

@@ -6,8 +6,8 @@ import {
} from '@tanstack/react-query-persist-client';
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
import { QueryCache, QueryClient, hashKey } from '@tanstack/react-query';
import { useSession, type Session, signOut } from '@/lib/auth-client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useSession, type Session } from '@/lib/auth-client';
import type { AppRouter } from '@zero/server/trpc';
import { CACHE_BURST_KEY } from '@/lib/constants';
import type { PropsWithChildren } from 'react';
@@ -35,7 +35,15 @@ export const makeQueryClient = (session: Session | null) =>
onError: (err, { meta }) => {
if (meta && meta.noGlobalError === true) return;
if (meta && typeof meta.customError === 'string') toast.error(meta.customError);
else toast.error(err.message || 'Something went wrong');
else if (err.message === 'Required scopes missing') {
signOut({
fetchOptions: {
onSuccess: () => {
window.location.href = '/login?error=required-scopes-missing';
},
},
});
} else toast.error(err.message || 'Something went wrong');
},
}),
defaultOptions: {

View File

@@ -1002,6 +1002,7 @@ export class GoogleMailManager implements MailManager {
return await Promise.resolve(fn());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
console.error(error);
const isFatal = FatalErrors.includes(error.message);
console.error(
`[${isFatal ? 'FATAL_ERROR' : 'ERROR'}] [Gmail Driver] Operation: ${operation}`,

View File

@@ -7,10 +7,10 @@ import { and, eq } from 'drizzle-orm';
export const FatalErrors = ['invalid_grant'];
export const deleteActiveConnection = async (c: HonoContext) => {
console.log('DELETEME');
const session = await c.var.auth.api.getSession({ headers: c.req.raw.headers });
if (!session?.connectionId) return console.log('No connection ID found');
try {
await c.var.auth.api.signOut({ headers: c.req.raw.headers });
await c.var.db
.delete(connection)
.where(and(eq(connection.userId, session.user.id), eq(connection.id, session.connectionId)));

View File

@@ -48,12 +48,14 @@ export const mailRouter = router({
const drafts = await driver.listDrafts({ q, maxResults: max, pageToken: cursor });
return drafts;
}
console.log('tr123');
const threadsResponse = await driver.list({
folder,
query: q,
maxResults: max,
pageToken: cursor,
});
console.log('tr246');
return threadsResponse;
}),
markAsRead: activeDriverProcedure

View File

@@ -1,6 +1,7 @@
import { connectionToDriver, getActiveConnection } from '../lib/server-utils';
import { Ratelimit, type RatelimitConfig } from '@upstash/ratelimit';
import type { HonoContext, HonoVariables } from '../ctx';
import { StandardizedError } from '../lib/driver/utils';
import { initTRPC, TRPCError } from '@trpc/server';
import { env } from 'cloudflare:workers';
import { redis } from '../lib/services';
@@ -35,7 +36,18 @@ export const activeConnectionProcedure = privateProcedure.use(async ({ ctx, next
export const activeDriverProcedure = activeConnectionProcedure.use(async ({ ctx, next }) => {
const { activeConnection } = ctx;
const driver = connectionToDriver(activeConnection, ctx.c);
return next({ ctx: { ...ctx, driver } });
const res = await next({ ctx: { ...ctx, driver } });
// This is for when the user has not granted the required scopes for GMail
if (!res.ok && res.error.message === 'Precondition check failed.') {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Required scopes missing',
cause: res.error,
});
}
return res;
});
export const brainServerAvailableMiddleware = t.middleware(async ({ next, ctx }) => {