chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
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 type { DashboardPreferences } from "~/services/dashboardPreferences.server";
|
||||
import { getDashboardPreferences } from "~/services/dashboardPreferences.server";
|
||||
export type { User } from "@trigger.dev/database";
|
||||
import { assertEmailAllowed } from "~/utils/email";
|
||||
import { emailMatchesPattern } from "~/utils/emailPattern";
|
||||
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 FindOrCreateSso = {
|
||||
authenticationMethod: "SSO";
|
||||
email: User["email"];
|
||||
firstName: string | null;
|
||||
lastName: string | null;
|
||||
};
|
||||
|
||||
type FindOrCreateUser =
|
||||
| FindOrCreateMagicLink
|
||||
| FindOrCreateGithub
|
||||
| FindOrCreateGoogle
|
||||
| FindOrCreateSso;
|
||||
|
||||
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);
|
||||
}
|
||||
case "SSO": {
|
||||
return findOrCreateSsoUser(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function findOrCreateMagicLinkUser({
|
||||
email,
|
||||
}: FindOrCreateMagicLink): Promise<LoggedInUser> {
|
||||
assertEmailAllowed(email);
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
const makeAdmin = env.ADMIN_EMAILS ? emailMatchesPattern(env.ADMIN_EMAILS, 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,
|
||||
};
|
||||
}
|
||||
|
||||
// Find an existing user by email (lowercased) or create a new one with the
|
||||
// SSO authentication method. Mirrors the magic-link upsert shape; the
|
||||
// callback route is responsible for normalising email before calling.
|
||||
// Plugin writes (linking the IdP identity row) happen via the SSO plugin
|
||||
// after this returns.
|
||||
export async function findOrCreateSsoUser({
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
}: FindOrCreateSso): Promise<LoggedInUser> {
|
||||
// Validate the canonical value we actually look up and persist below —
|
||||
// validating raw `email` would let case/whitespace variants slip past
|
||||
// (or misapply) the allow-list policy.
|
||||
const normalised = email.toLowerCase().trim();
|
||||
assertEmailAllowed(normalised);
|
||||
|
||||
const existingUser = await prisma.user.findFirst({ where: { email: normalised } });
|
||||
|
||||
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim() || null;
|
||||
|
||||
const user = await prisma.user.upsert({
|
||||
where: { email: normalised },
|
||||
update: {
|
||||
// Existing magic-link / OAuth users keep their original
|
||||
// authenticationMethod; we only refresh name/displayName when the
|
||||
// user has nothing set yet so we don't clobber a customised display
|
||||
// name on every SSO login.
|
||||
...(existingUser?.name ? {} : { name: fullName }),
|
||||
...(existingUser?.displayName ? {} : { displayName: fullName }),
|
||||
},
|
||||
create: {
|
||||
email: normalised,
|
||||
name: fullName,
|
||||
displayName: fullName,
|
||||
authenticationMethod: "SSO",
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user