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
10 changes: 9 additions & 1 deletion apps/web/app/api/outlook/linking/callback/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import prisma from "@/utils/prisma";
import { createScopedLogger } from "@/utils/logger";
import { OUTLOOK_LINKING_STATE_COOKIE_NAME } from "@/utils/outlook/constants";
import { withError } from "@/utils/middleware";
import { SafeError } from "@/utils/error";
import { captureException, SafeError } from "@/utils/error";
import { transferPremiumDuringMerge } from "@/utils/user/merge-premium";
import { parseOAuthState } from "@/utils/oauth/state";

Expand Down Expand Up @@ -76,6 +76,14 @@ export const GET = withError(async (request) => {
const tokens = await tokenResponse.json();

if (!tokenResponse.ok) {
logger.error("Failed to exchange code for tokens", {
error: tokens.error_description,
});
captureException(
new Error(
tokens.error_description || "Failed to exchange code for tokens",
),
);
throw new Error(
tokens.error_description || "Failed to exchange code for tokens",
);
Expand Down
26 changes: 11 additions & 15 deletions apps/web/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { sso } from "@better-auth/sso";
import { createContact as createLoopsContact } from "@inboxzero/loops";
import { createContact as createResendContact } from "@inboxzero/resend";
import type { Prisma } from "@prisma/client";
import type { Account, AuthContext, User } from "better-auth";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
Expand Down Expand Up @@ -375,24 +374,21 @@ async function handleLinkAccount(account: Account) {
return;
}

// --- Create/Update the corresponding EmailAccount record ---
const emailAccountData: Prisma.EmailAccountUpsertArgs = {
const data = {
userId: account.userId,
accountId: account.id,
name: primaryName,
image: primaryPhotoUrl,
};

await prisma.emailAccount.upsert({
where: { email: profileData?.email },
update: {
userId: account.userId,
accountId: account.id,
name: primaryName,
image: primaryPhotoUrl,
},
update: data,
create: {
...data,
email: primaryEmail,
userId: account.userId,
accountId: account.id,
name: primaryName,
image: primaryPhotoUrl,
},
};
await prisma.emailAccount.upsert(emailAccountData);
});

// Handle premium account seats
await updateAccountSeats({ userId: account.userId }).catch((error) => {
Expand Down
1 change: 1 addition & 0 deletions apps/web/utils/email/watch-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ async function watchEmailAccounts(
"invalid_grant",
"Mail service not enabled",
"Insufficient Permission",
"AADSTS7000215", // Raw Azure AD error for invalid client secret (old tokens after secret rotation)
];

if (warn.some((w) => error.message.includes(w))) {
Expand Down
8 changes: 1 addition & 7 deletions apps/web/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,13 +185,7 @@ function processErrorsInObject(obj: unknown): unknown {
}

// Field names that contain PII and should be hashed in production
const SENSITIVE_FIELD_NAMES = new Set([
"email",
"from",
"sender",
"to",
"userEmail",
]);
const SENSITIVE_FIELD_NAMES = new Set(["from", "sender", "to"]);
Comment thread
elie222 marked this conversation as resolved.

// Field names that should NEVER be logged - replaced with boolean
const REDACTED_FIELD_NAMES = new Set([
Expand Down
18 changes: 17 additions & 1 deletion apps/web/utils/outlook/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,23 @@ export const getOutlookClientWithRefresh = async ({
const tokens = await response.json();

if (!response.ok) {
throw new Error(tokens.error_description || "Failed to refresh token");
const errorMessage =
tokens.error_description || "Failed to refresh token";

// AADSTS7000215 = Invalid client secret
// Happens when Azure AD client secret rotates or refresh token expires
// Background processes (watch-manager) will catch and log this as a warning
// User-facing flows will show an error prompting reconnection
if (errorMessage.includes("AADSTS7000215")) {
logger.warn(
"Microsoft refresh token failed - user may need to reconnect",
{
emailAccountId,
},
);
}

throw new Error(errorMessage);
}

// Save new tokens
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.18.11
v2.18.13
Loading