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
1 change: 1 addition & 0 deletions apps/web/app/api/google/linking/auth-url/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we want the user's consent due to the added scopes.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's needed. consent just means if they need to reconsent. If they hadn't given consent they'd see the consent screen anyway. But it's fine to keep this in anyway. It means the user is forced to give consent again even if they already gave consent.

state,
});

Expand Down
90 changes: 59 additions & 31 deletions apps/web/utils/gmail/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increased the error list for attempting to acquire a new access 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 {
Expand All @@ -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 },
Comment thread
edulelis marked this conversation as resolved.
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", {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should never happen, but added a log here to observe.

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;
}
Loading