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
3 changes: 2 additions & 1 deletion .claude/commands/create-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,5 @@ gh pr create --title "<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.
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.
48 changes: 35 additions & 13 deletions apps/web/app/api/google/linking/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
elie222 marked this conversation as resolved.
} else {
throw createError;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
},
});
}
74 changes: 51 additions & 23 deletions apps/web/app/api/outlook/linking/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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,
Expand Down Expand Up @@ -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,
},
});
}
Loading