bff4b46b22
## Summary A "Google auth conflict" Sentry alert fires whenever a user signs in via Google whose Google account is linked to one user row but whose Google-provided email is now on a *different* user row. The handler in `apps/webapp/app/models/user.server.ts:236` already does the right thing — it returns the existing auth-linked user and skips the update path so neither row gets mutated — but it logs the situation with `logger.error`, which routes to Sentry as an exception and pages the on-call channel. There's no exception to chase here: the branch is the intended outcome for a known data shape (user changed their email on one account after originally signing up via Google on another). Downgrading the call to `logger.warn` keeps the diagnostic record in our logs (with all the same context fields — email, both user IDs, authIdentifier) but stops it firing the production error alert. ## Change - `logger.error` → `logger.warn` for the conflict branch in `findOrCreateGoogleUser`. Context payload is unchanged. ## Test plan - [x] Typecheck only — there's no behavioural change to test, the log level is the entire diff.
359 lines
8.5 KiB
TypeScript
359 lines
8.5 KiB
TypeScript
import type { Prisma, User } from "@trigger.dev/database";
|
|
import type { GitHubProfile } from "remix-auth-github";
|
|
import type { GoogleProfile } from "remix-auth-google";
|
|
import { prisma } from "~/db.server";
|
|
import { env } from "~/env.server";
|
|
import {
|
|
DashboardPreferences,
|
|
getDashboardPreferences,
|
|
} from "~/services/dashboardPreferences.server";
|
|
export type { User } from "@trigger.dev/database";
|
|
import { assertEmailAllowed } from "~/utils/email";
|
|
import { logger } from "~/services/logger.server";
|
|
|
|
type FindOrCreateMagicLink = {
|
|
authenticationMethod: "MAGIC_LINK";
|
|
email: string;
|
|
};
|
|
|
|
type FindOrCreateGithub = {
|
|
authenticationMethod: "GITHUB";
|
|
email: User["email"];
|
|
authenticationProfile: GitHubProfile;
|
|
authenticationExtraParams: Record<string, unknown>;
|
|
};
|
|
|
|
type FindOrCreateGoogle = {
|
|
authenticationMethod: "GOOGLE";
|
|
email: User["email"];
|
|
authenticationProfile: GoogleProfile;
|
|
authenticationExtraParams: Record<string, unknown>;
|
|
};
|
|
|
|
type FindOrCreateUser = FindOrCreateMagicLink | FindOrCreateGithub | FindOrCreateGoogle;
|
|
|
|
type LoggedInUser = {
|
|
user: User;
|
|
isNewUser: boolean;
|
|
};
|
|
|
|
export async function findOrCreateUser(input: FindOrCreateUser): Promise<LoggedInUser> {
|
|
switch (input.authenticationMethod) {
|
|
case "GITHUB": {
|
|
return findOrCreateGithubUser(input);
|
|
}
|
|
case "MAGIC_LINK": {
|
|
return findOrCreateMagicLinkUser(input);
|
|
}
|
|
case "GOOGLE": {
|
|
return findOrCreateGoogleUser(input);
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function findOrCreateMagicLinkUser({
|
|
email,
|
|
}: FindOrCreateMagicLink): Promise<LoggedInUser> {
|
|
assertEmailAllowed(email);
|
|
|
|
const existingUser = await prisma.user.findFirst({
|
|
where: {
|
|
email,
|
|
},
|
|
});
|
|
|
|
const adminEmailRegex = env.ADMIN_EMAILS ? new RegExp(env.ADMIN_EMAILS) : undefined;
|
|
const makeAdmin = adminEmailRegex ? adminEmailRegex.test(email) : false;
|
|
|
|
const user = await prisma.user.upsert({
|
|
where: {
|
|
email,
|
|
},
|
|
update: {
|
|
email,
|
|
},
|
|
create: {
|
|
email,
|
|
authenticationMethod: "MAGIC_LINK",
|
|
admin: makeAdmin, // only on create, to prevent automatically removing existing admins
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: !existingUser,
|
|
};
|
|
}
|
|
|
|
export async function findOrCreateGithubUser({
|
|
email,
|
|
authenticationProfile,
|
|
authenticationExtraParams,
|
|
}: FindOrCreateGithub): Promise<LoggedInUser> {
|
|
assertEmailAllowed(email);
|
|
|
|
const name = authenticationProfile._json.name;
|
|
let avatarUrl: string | undefined = undefined;
|
|
if (authenticationProfile.photos[0]) {
|
|
avatarUrl = authenticationProfile.photos[0].value;
|
|
}
|
|
const displayName = authenticationProfile.displayName;
|
|
const authProfile = authenticationProfile
|
|
? (authenticationProfile as unknown as Prisma.JsonObject)
|
|
: undefined;
|
|
const authExtraParams = authenticationExtraParams
|
|
? (authenticationExtraParams as unknown as Prisma.JsonObject)
|
|
: undefined;
|
|
|
|
const authIdentifier = `github:${authenticationProfile.id}`;
|
|
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: {
|
|
authIdentifier,
|
|
},
|
|
});
|
|
|
|
const existingEmailUser = await prisma.user.findUnique({
|
|
where: {
|
|
email,
|
|
},
|
|
});
|
|
|
|
if (existingEmailUser && !existingUser) {
|
|
const user = await prisma.user.update({
|
|
where: {
|
|
email,
|
|
},
|
|
data: {
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
avatarUrl,
|
|
authIdentifier,
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: false,
|
|
};
|
|
}
|
|
|
|
if (existingEmailUser && existingUser) {
|
|
const user = await prisma.user.update({
|
|
where: {
|
|
id: existingUser.id,
|
|
},
|
|
data: {},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: false,
|
|
};
|
|
}
|
|
|
|
const user = await prisma.user.upsert({
|
|
where: {
|
|
authIdentifier,
|
|
},
|
|
update: {},
|
|
create: {
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
name,
|
|
avatarUrl,
|
|
displayName,
|
|
authIdentifier,
|
|
email,
|
|
authenticationMethod: "GITHUB",
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: !existingUser,
|
|
};
|
|
}
|
|
|
|
export async function findOrCreateGoogleUser({
|
|
email,
|
|
authenticationProfile,
|
|
authenticationExtraParams,
|
|
}: FindOrCreateGoogle): Promise<LoggedInUser> {
|
|
assertEmailAllowed(email);
|
|
|
|
const name = authenticationProfile._json.name;
|
|
let avatarUrl: string | undefined = undefined;
|
|
if (authenticationProfile.photos[0]) {
|
|
avatarUrl = authenticationProfile.photos[0].value;
|
|
}
|
|
const displayName = authenticationProfile.displayName;
|
|
const authProfile = authenticationProfile
|
|
? (authenticationProfile as unknown as Prisma.JsonObject)
|
|
: undefined;
|
|
const authExtraParams = authenticationExtraParams
|
|
? (authenticationExtraParams as unknown as Prisma.JsonObject)
|
|
: undefined;
|
|
|
|
const authIdentifier = `google:${authenticationProfile.id}`;
|
|
|
|
const existingUser = await prisma.user.findUnique({
|
|
where: {
|
|
authIdentifier,
|
|
},
|
|
});
|
|
|
|
const existingEmailUser = await prisma.user.findUnique({
|
|
where: {
|
|
email,
|
|
},
|
|
});
|
|
|
|
if (existingEmailUser && !existingUser) {
|
|
// Link existing email account to Google auth, preserving original authenticationMethod
|
|
const user = await prisma.user.update({
|
|
where: {
|
|
email,
|
|
},
|
|
data: {
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
avatarUrl,
|
|
authIdentifier,
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: false,
|
|
};
|
|
}
|
|
|
|
if (existingEmailUser && existingUser) {
|
|
// Check if email user and auth user are the same
|
|
if (existingEmailUser.id !== existingUser.id) {
|
|
// Different users: email is taken by one user, Google auth belongs to another
|
|
logger.warn(
|
|
`Google auth conflict: Google ID ${authenticationProfile.id} belongs to user ${existingUser.id} but email ${email} is taken by user ${existingEmailUser.id}`,
|
|
{
|
|
email,
|
|
existingEmailUserId: existingEmailUser.id,
|
|
existingAuthUserId: existingUser.id,
|
|
authIdentifier,
|
|
}
|
|
);
|
|
|
|
return {
|
|
user: existingUser,
|
|
isNewUser: false,
|
|
};
|
|
}
|
|
|
|
// Same user: update all profile fields
|
|
const user = await prisma.user.update({
|
|
where: {
|
|
id: existingUser.id,
|
|
},
|
|
data: {
|
|
email,
|
|
displayName,
|
|
name,
|
|
avatarUrl,
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: false,
|
|
};
|
|
}
|
|
|
|
// When the IDP user (Google) already exists, the "update" path will be taken and the email will be updated
|
|
// It's not possible that the email is already taken by a different user because that would have been handled
|
|
// by one of the if statements above.
|
|
const user = await prisma.user.upsert({
|
|
where: {
|
|
authIdentifier,
|
|
},
|
|
update: {
|
|
email,
|
|
displayName,
|
|
name,
|
|
avatarUrl,
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
},
|
|
create: {
|
|
authenticationProfile: authProfile,
|
|
authenticationExtraParams: authExtraParams,
|
|
name,
|
|
avatarUrl,
|
|
displayName,
|
|
authIdentifier,
|
|
email,
|
|
authenticationMethod: "GOOGLE",
|
|
},
|
|
});
|
|
|
|
return {
|
|
user,
|
|
isNewUser: !existingUser,
|
|
};
|
|
}
|
|
|
|
export type UserWithDashboardPreferences = User & {
|
|
dashboardPreferences: DashboardPreferences;
|
|
};
|
|
|
|
export async function getUserById(id: User["id"]) {
|
|
const user = await prisma.user.findUnique({ where: { id } });
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
const dashboardPreferences = getDashboardPreferences(user.dashboardPreferences);
|
|
|
|
return {
|
|
...user,
|
|
dashboardPreferences,
|
|
};
|
|
}
|
|
|
|
export async function getUserByEmail(email: User["email"]) {
|
|
return prisma.user.findUnique({ where: { email } });
|
|
}
|
|
|
|
export function updateUser({
|
|
id,
|
|
name,
|
|
email,
|
|
marketingEmails,
|
|
referralSource,
|
|
onboardingData,
|
|
}: Pick<User, "id" | "name" | "email"> & {
|
|
marketingEmails?: boolean;
|
|
referralSource?: string;
|
|
onboardingData?: Prisma.InputJsonValue;
|
|
}) {
|
|
return prisma.user.update({
|
|
where: { id },
|
|
data: { name, email, marketingEmails, referralSource, onboardingData, confirmedBasicDetails: true },
|
|
});
|
|
}
|
|
|
|
export async function grantUserCloudAccess({ id, inviteCode }: { id: string; inviteCode: string }) {
|
|
return prisma.user.update({
|
|
where: { id },
|
|
data: {
|
|
invitationCode: {
|
|
connect: {
|
|
code: inviteCode,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|