diff --git a/apps/web/app/api/google/linking/auth-url/route.ts b/apps/web/app/api/google/linking/auth-url/route.ts index af22452591..71df65c785 100644 --- a/apps/web/app/api/google/linking/auth-url/route.ts +++ b/apps/web/app/api/google/linking/auth-url/route.ts @@ -16,6 +16,7 @@ const getAuthUrl = ({ userId }: { userId: string }) => { const url = googleAuth.generateAuthUrl({ access_type: "offline", scope: [...new Set([...SCOPES, "openid", "email"])].join(" "), + prompt: "consent", state, }); diff --git a/apps/web/utils/gmail/permissions.ts b/apps/web/utils/gmail/permissions.ts index 5ad6ccd510..b6e93a4eb8 100644 --- a/apps/web/utils/gmail/permissions.ts +++ b/apps/web/utils/gmail/permissions.ts @@ -81,10 +81,20 @@ export async function handleGmailPermissionsCheck({ refreshToken: string | null | undefined; emailAccountId: string; }) { - const { hasAllPermissions, error, missingScopes } = - await checkGmailPermissions({ accessToken, emailAccountId }); + const permissionsBeforeRefresh = await checkGmailPermissions({ + accessToken, + emailAccountId, + }); - if (error === "invalid_token") { + if ( + permissionsBeforeRefresh.error && + [ + "invalid_token", + "invalid_grant", + "invalid_scope", + "access_denied", + ].includes(permissionsBeforeRefresh.error) + ) { // attempt to refresh the token one last time using only the refresh token if (refreshToken) { try { @@ -97,39 +107,57 @@ export async function handleGmailPermissionsCheck({ }); // re-check permissions with the new access token - const newAccessToken = getAccessTokenFromClient(gmailClient); - return await checkGmailPermissions({ - accessToken: newAccessToken, + const accessToken = getAccessTokenFromClient(gmailClient); + const permissionsAfterRefresh = await checkGmailPermissions({ + accessToken, emailAccountId, }); + + if ( + permissionsAfterRefresh.error && + permissionsAfterRefresh.error === "invalid_grant" + ) { + logger.info("Cleaning up invalid Gmail tokens", { emailAccountId }); + const emailAccount = await prisma.emailAccount.findUnique({ + where: { id: emailAccountId }, + select: { accountId: true }, + }); + if (!emailAccount) + return { + hasAllPermissions: false, + error: "Email account not found", + }; + + await prisma.account.update({ + where: { id: emailAccount.accountId }, + data: { + access_token: null, + refresh_token: null, + expires_at: null, + }, + }); + + return { + hasAllPermissions: false, + error: "Gmail access expired. Please reconnect your account.", + missingScopes: permissionsBeforeRefresh.missingScopes, + }; + } + + return permissionsAfterRefresh; } catch (_) { - // getGmailClientWithRefresh, getAccessTokenFromClient will throw if access token is invalid - // Refresh failed, fall through to cleanup + return { + hasAllPermissions: false, + error: "Gmail access expired. Please reconnect your account.", + missingScopes: permissionsBeforeRefresh.missingScopes, + }; } + } else { + logger.warn("Got no refresh token to attempt refresh", { + emailAccountId, + }); } - - // Clean up invalid Gmail tokens (either no refresh token or refresh failed) - logger.info("Cleaning up invalid Gmail tokens", { emailAccountId }); - const emailAccount = await prisma.emailAccount.findUnique({ - where: { id: emailAccountId }, - }); - if (!emailAccount) - return { hasAllPermissions: false, error: "Email account not found" }; - - await prisma.account.update({ - where: { id: emailAccount.accountId }, - data: { - access_token: null, - refresh_token: null, - expires_at: null, - }, - }); - return { - hasAllPermissions: false, - error: "Gmail access expired. Please reconnect your account.", - missingScopes, - }; } - return { hasAllPermissions, error, missingScopes }; + return permissionsBeforeRefresh; }