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
18 changes: 18 additions & 0 deletions apps/web/app/(landing)/logout/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"use client";

import { useEffect } from "react";
import { logOut } from "@/utils/user";
import { Loading } from "@/components/Loading";
import { BasicLayout } from "@/components/layouts/BasicLayout";

export default function LogoutPage() {
useEffect(() => {
logOut("/login");
}, []);

return (
<BasicLayout>
<Loading />
</BasicLayout>
);
}
3 changes: 2 additions & 1 deletion apps/web/app/(landing)/welcome-redirect/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export default async function WelcomeRedirectPage(props: {
select: { completedOnboardingAt: true, utms: true },
});

if (!user) redirect("/login");
// Session exists but user doesn't - invalid state, log out
if (!user) redirect("/logout");
if (searchParams.force) redirect("/onboarding");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

searchParams.force is a string, so if (searchParams.force) treats "false" as truthy and redirects. Consider coercing to a boolean (e.g., compare to "true") before branching.

Suggested change
if (searchParams.force) redirect("/onboarding");
if (searchParams.force === "true") redirect("/onboarding");

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

if (user.completedOnboardingAt) redirect("/setup");
redirect("/onboarding");
Expand Down
196 changes: 108 additions & 88 deletions apps/web/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { sso } from "@better-auth/sso";
import { createContact as createLoopsContact } from "@inboxzero/loops";
import { createContact as createResendContact } from "@inboxzero/resend";
import type { Account, AuthContext, User } from "better-auth";
import type { Account, AuthContext } from "better-auth";
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { nextCookies } from "better-auth/next-js";
Expand Down Expand Up @@ -110,10 +110,22 @@ export const betterAuthConfig = betterAuth({
disableIdTokenSignIn: true,
},
},
events: {
signIn: handleSignIn,
},
databaseHooks: {
user: {
create: {
after: async (user) => {
await postSignUp({
id: user.id,
email: user.email,
name: user.name,
image: user.image,
}).catch((error) => {
logger.error("Error posting sign up", { error, user });
captureException(error, { extra: { user } });
});
},
},
},
account: {
create: {
after: async (account: Account) => {
Expand All @@ -136,106 +148,114 @@ export const betterAuthConfig = betterAuth({
},
});

async function handleSignIn({
user,
isNewUser,
async function postSignUp({
id: userId,
email,
name,
image,
}: {
user: User;
isNewUser: boolean;
id: string;
email: string;
name?: string | null;
image?: string | null;
}) {
if (isNewUser && user.email) {
const loops = async () => {
const account = await prisma.account
.findFirst({
where: { userId: user.id },
select: { provider: true },
})
.catch((error) => {
logger.error("Error finding account", {
userId: user.id,
error,
});
captureException(error, undefined, user.email);
const loops = async () => {
const account = await prisma.account
.findFirst({
where: { userId },
select: { provider: true },
})
.catch((error) => {
logger.error("Error finding account", {
userId,
error,
});

await createLoopsContact(
user.email,
user.name?.split(" ")?.[0],
account?.provider,
).catch((error) => {
const alreadyExists =
error instanceof Error && error.message.includes("409");
if (!alreadyExists) {
logger.error("Error creating Loops contact", {
email: user.email,
error,
});
captureException(error, undefined, user.email);
}
captureException(error, undefined, email);
});
};

const resend = createResendContact({ email: user.email }).catch((error) => {
logger.error("Error creating Resend contact", {
email: user.email,
error,
});
captureException(error, undefined, user.email);
await createLoopsContact(
email,
name?.split(" ")?.[0],
account?.provider,
).catch((error) => {
const alreadyExists =
error instanceof Error && error.message.includes("409");
if (!alreadyExists) {
logger.error("Error creating Loops contact", {
email,
error,
});
captureException(error, undefined, email);
}
});
};
Comment on lines +162 to +191
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fragile error handling for duplicate contact detection.

The error check on lines 181-183 uses error.message.includes("409") to detect duplicate contacts, which is fragile and could break if the error message format changes.

Consider checking the HTTP status code directly if the error object provides it:

-    const alreadyExists =
-      error instanceof Error && error.message.includes("409");
+    const alreadyExists =
+      (error as any)?.response?.status === 409 ||
+      (error instanceof Error && error.message.includes("409"));

Alternatively, check the Loops API client documentation to see if it throws a specific error type for conflicts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/utils/auth.ts around lines 162 to 191, the duplicate-contact
detection is fragile because it checks error.message.includes("409"); instead,
inspect the error object for a concrete status/code or specific error type: if
the Loops client or HTTP client exposes error.status or error.response?.status,
check that === 409 (or use the client’s Conflict error class), or import and use
the Loops-specific error type/utility to detect conflicts; update the catch to
branch on that status/type and only log/capture non-conflict errors.


const dub = trackDubSignUp(user).catch((error) => {
const resend = createResendContact({ email }).catch((error) => {
logger.error("Error creating Resend contact", {
email,
error,
});
captureException(error, undefined, email);
});

const dub = trackDubSignUp({ id: userId, email, name, image }).catch(
(error) => {
logger.error("Error tracking Dub sign up", {
email: user.email,
email,
error,
});
captureException(error, undefined, user.email);
});

await Promise.all([loops(), resend, dub]);
}

if (isNewUser && user.email && user.id) {
await Promise.all([
handlePendingPremiumInvite({ email: user.email }),
handleReferralOnSignUp({
userId: user.id,
email: user.email,
}),
]);
}
captureException(error, undefined, email);
},
);

await Promise.all([
loops(),
resend,
dub,
handlePendingPremiumInvite({ email }),
handleReferralOnSignUp({ userId, email }),
]);
}

async function handlePendingPremiumInvite({ email }: { email: string }) {
logger.info("Handling pending premium invite", { email });

// Check for pending invite
const premium = await prisma.premium.findFirst({
where: { pendingInvites: { has: email } },
select: {
id: true,
pendingInvites: true,
lemonSqueezySubscriptionItemId: true,
stripeSubscriptionId: true,
_count: { select: { users: true } },
},
});
try {
logger.info("Handling pending premium invite", { email });

// Check for pending invite
const premium = await prisma.premium.findFirst({
where: { pendingInvites: { has: email } },
select: {
id: true,
pendingInvites: true,
lemonSqueezySubscriptionItemId: true,
stripeSubscriptionId: true,
_count: { select: { users: true } },
},
});

if (
premium?.lemonSqueezySubscriptionItemId ||
premium?.stripeSubscriptionId
) {
// Add user to premium and remove from pending invites
await prisma.premium.update({
where: { id: premium.id },
data: {
users: { connect: { email } },
pendingInvites: {
set: premium.pendingInvites.filter((e: string) => e !== email),
if (
premium?.lemonSqueezySubscriptionItemId ||
premium?.stripeSubscriptionId
) {
// Add user to premium and remove from pending invites
await prisma.premium.update({
where: { id: premium.id },
data: {
users: { connect: { email } },
pendingInvites: {
set: premium.pendingInvites.filter((e: string) => e !== email),
},
},
},
});

logger.info("Added user to premium from invite", { email });
}
} catch (error) {
logger.error("Error handling pending premium invite", { error, email });
captureException(error, {
extra: { email, location: "handlePendingPremiumInvite" },
});
}

logger.info("Added user to premium from invite", { email });
}

export async function handleReferralOnSignUp({
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.21.52
v2.21.54
Loading