diff --git a/.claude/commands/create-pr.md b/.claude/commands/create-pr.md index 920d5c770f..f2902057b5 100644 --- a/.claude/commands/create-pr.md +++ b/.claude/commands/create-pr.md @@ -58,4 +58,5 @@ gh pr create --title "" --body "<body>" gh pr create --title "<title>" --body "<body>" && gh pr comment $(gh pr view --json number -q .number) --body "#skipreview" ``` -Display the returned PR URL as a markdown link on its own line, formatted as: `[PR #<number>](<url>)` so it's clickable. \ No newline at end of file +Display the returned PR URL as a markdown link on its own line, formatted as: `[PR #<number>](<url>)` so it's clickable. +Display the name of the branch you created. \ No newline at end of file diff --git a/apps/web/app/api/google/linking/callback/route.ts b/apps/web/app/api/google/linking/callback/route.ts index 8b9be8f160..ec620cad2c 100644 --- a/apps/web/app/api/google/linking/callback/route.ts +++ b/apps/web/app/api/google/linking/callback/route.ts @@ -179,17 +179,20 @@ export const GET = withError("google/linking/callback", async (request) => { providerAccountId, }, }, - select: { userId: true }, + select: { id: true, userId: true }, }); if (accountNow?.userId === targetUserId) { logger.info( - "Account was created by concurrent request, continuing", + "Account already exists for same user, updating tokens", { targetUserId, providerAccountId, + accountId: accountNow.id, }, ); + + await updateGoogleAccountTokens(accountNow.id, tokens); } else { throw createError; } @@ -215,17 +218,7 @@ export const GET = withError("google/linking/callback", async (request) => { accountId: linkingResult.existingAccountId, }); - await prisma.account.update({ - where: { id: linkingResult.existingAccountId }, - data: { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - expires_at: tokens.expiry_date ? new Date(tokens.expiry_date) : null, - scope: tokens.scope, - token_type: tokens.token_type, - id_token: tokens.id_token, - }, - }); + await updateGoogleAccountTokens(linkingResult.existingAccountId, tokens); logger.info("Successfully updated tokens for Google account", { email: providerEmail, @@ -291,3 +284,32 @@ export const GET = withError("google/linking/callback", async (request) => { }); } }); + +interface GoogleTokens { + access_token?: string | null; + refresh_token?: string | null; + expiry_date?: number | null; + scope?: string | null; + token_type?: string | null; + id_token?: string | null; +} + +async function updateGoogleAccountTokens( + accountId: string, + tokens: GoogleTokens, +) { + await prisma.account.update({ + where: { id: accountId }, + data: { + access_token: tokens.access_token, + // Only update refresh_token if provider returned one (preserves existing token) + ...(tokens.refresh_token != null && { + refresh_token: tokens.refresh_token, + }), + expires_at: tokens.expiry_date ? new Date(tokens.expiry_date) : null, + scope: tokens.scope, + token_type: tokens.token_type, + id_token: tokens.id_token, + }, + }); +} diff --git a/apps/web/app/api/outlook/linking/callback/route.ts b/apps/web/app/api/outlook/linking/callback/route.ts index 5607eada9e..0fff63e6e6 100644 --- a/apps/web/app/api/outlook/linking/callback/route.ts +++ b/apps/web/app/api/outlook/linking/callback/route.ts @@ -231,17 +231,20 @@ export const GET = withError("outlook/linking/callback", async (request) => { providerAccountId, }, }, - select: { userId: true }, + select: { id: true, userId: true }, }); if (accountNow?.userId === targetUserId) { logger.info( - "Account was created by concurrent request, continuing", + "Account already exists for same user, updating tokens", { targetUserId, providerAccountId, + accountId: accountNow.id, }, ); + + await updateMicrosoftAccountTokens(accountNow.id, tokens); } else { throw createError; } @@ -267,27 +270,10 @@ export const GET = withError("outlook/linking/callback", async (request) => { accountId: linkingResult.existingAccountId, }); - let expiresAt: Date | null = null; - if (tokens.expires_at) { - expiresAt = new Date(tokens.expires_at * 1000); - } else if (tokens.expires_in) { - const expiresInSeconds = - typeof tokens.expires_in === "string" - ? Number.parseInt(tokens.expires_in, 10) - : tokens.expires_in; - expiresAt = new Date(Date.now() + expiresInSeconds * 1000); - } - - await prisma.account.update({ - where: { id: linkingResult.existingAccountId }, - data: { - access_token: tokens.access_token, - refresh_token: tokens.refresh_token, - expires_at: expiresAt, - scope: tokens.scope, - token_type: tokens.token_type, - }, - }); + await updateMicrosoftAccountTokens( + linkingResult.existingAccountId, + tokens, + ); logger.info("Successfully updated tokens for Microsoft account", { email: providerEmail, @@ -351,3 +337,45 @@ export const GET = withError("outlook/linking/callback", async (request) => { }); } }); + +interface MicrosoftTokens { + access_token: string; + refresh_token?: string | null; + expires_at?: number; + expires_in?: string | number; + scope?: string | null; + token_type?: string | null; +} + +function parseMicrosoftExpiresAt(tokens: MicrosoftTokens): Date | null { + if (tokens.expires_at) { + return new Date(tokens.expires_at * 1000); + } + if (tokens.expires_in) { + const expiresInSeconds = + typeof tokens.expires_in === "string" + ? Number.parseInt(tokens.expires_in, 10) + : tokens.expires_in; + return new Date(Date.now() + expiresInSeconds * 1000); + } + return null; +} + +async function updateMicrosoftAccountTokens( + accountId: string, + tokens: MicrosoftTokens, +) { + await prisma.account.update({ + where: { id: accountId }, + data: { + access_token: tokens.access_token, + // Only update refresh_token if provider returned one (preserves existing token) + ...(tokens.refresh_token != null && { + refresh_token: tokens.refresh_token, + }), + expires_at: parseMicrosoftExpiresAt(tokens), + scope: tokens.scope, + token_type: tokens.token_type, + }, + }); +}