Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve transactions speed for user creation #308

Draft
wants to merge 75 commits into
base: dev
Choose a base branch
from
Draft
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
d7e3f95
removed contact channels from otp
fomalhautb Sep 29, 2024
97b94fd
fixed types
fomalhautb Sep 29, 2024
2928f5b
fixed bugs
fomalhautb Sep 29, 2024
cbaadee
fixed bug
fomalhautb Sep 29, 2024
32ed487
fixed bugs
fomalhautb Sep 29, 2024
02aed01
updated user contact channel
fomalhautb Sep 29, 2024
7cc1bff
updated tests
fomalhautb Sep 29, 2024
4bef06f
updated tests
fomalhautb Sep 29, 2024
d556a5e
added unique key to otp and password auth
fomalhautb Sep 30, 2024
8076a21
added contact channel api
fomalhautb Oct 1, 2024
e0b3219
added new send-verification-code route
fomalhautb Oct 1, 2024
e8dcca4
updated tests
fomalhautb Oct 1, 2024
28d4331
added contact channel create tests, fixed bug
fomalhautb Oct 1, 2024
74bf6f7
added more tests, removed update
fomalhautb Oct 1, 2024
587f077
added more tests
fomalhautb Oct 1, 2024
1842608
added more tests
fomalhautb Oct 1, 2024
6985443
added more tests
fomalhautb Oct 1, 2024
edda2b0
Merge branch 'dev' into contact-channel-api
fomalhautb Oct 1, 2024
684e396
fixed typecheck
fomalhautb Oct 1, 2024
e23648f
fixed route structure
fomalhautb Oct 1, 2024
e64f8c3
fixed bugs, fixed tests
fomalhautb Oct 1, 2024
780d61a
added more tests
fomalhautb Oct 1, 2024
f6cd6cf
added more tests
fomalhautb Oct 1, 2024
cd779dc
fixed tests
fomalhautb Oct 2, 2024
50381d2
fixed tests
fomalhautb Oct 2, 2024
2e591de
implemented new merge account logic
fomalhautb Oct 3, 2024
c1d4c81
moved user create of otp sign in to after the first email verification
fomalhautb Oct 3, 2024
412b5ac
added cc updates
fomalhautb Oct 3, 2024
569551f
fixed bugs
fomalhautb Oct 3, 2024
86bf3c7
added more tests
fomalhautb Oct 3, 2024
a6e3962
added more flows
fomalhautb Oct 3, 2024
898188e
uncomment tests
fomalhautb Oct 3, 2024
eef502f
added client functions
fomalhautb Oct 4, 2024
0241f63
added primary updates
fomalhautb Oct 5, 2024
97b452c
Merge branch 'contact-channel-api' into contact-channel-client
fomalhautb Oct 5, 2024
e015b57
added email ui
fomalhautb Oct 5, 2024
1452aa8
Merge branch 'dev' into contact-channel-api
fomalhautb Oct 9, 2024
0d22bb8
Merge branch 'contact-channel-api' into contact-channel-client
fomalhautb Oct 9, 2024
c8a3909
updated translation
fomalhautb Oct 9, 2024
c767912
added email table
fomalhautb Oct 10, 2024
4707c66
updated email adding ui, added send verification email to contact cha…
fomalhautb Oct 11, 2024
3bbcbb0
fixed bugs
fomalhautb Oct 11, 2024
8795af2
translate buttons
fomalhautb Oct 11, 2024
ee236dc
added otp auth enabled
fomalhautb Oct 12, 2024
af1abc7
updated ui
fomalhautb Oct 12, 2024
529ef2c
Update apps/e2e/tests/backend/endpoints/api/v1/contact-channels/legac…
fomalhautb Oct 13, 2024
183d889
Update apps/e2e/tests/backend/endpoints/api/v1/auth/otp/sign-in.test.ts
fomalhautb Oct 13, 2024
aecdbd2
updated test names
fomalhautb Oct 13, 2024
9a96e81
Merge branch 'contact-channel-api' of github.com:stackframe-projects/…
fomalhautb Oct 13, 2024
b93c855
Update apps/e2e/tests/backend/endpoints/api/v1/auth-flows.test.ts
fomalhautb Oct 13, 2024
fcafe2d
Update apps/e2e/tests/backend/endpoints/api/v1/auth-flows.test.ts
fomalhautb Oct 13, 2024
477ce45
Update apps/backend/src/app/api/v1/auth/oauth/callback/[provider_id]/…
fomalhautb Oct 13, 2024
792dac5
Merge branch 'dev' into contact-channel-api
fomalhautb Oct 13, 2024
5324f77
added set password and updated tests
fomalhautb Oct 13, 2024
9209f3b
updated tests
fomalhautb Oct 13, 2024
51a603b
fixed lint
fomalhautb Oct 13, 2024
345a8b2
fixed lint
fomalhautb Oct 13, 2024
068c87b
Merge branch 'contact-channel-api' into contact-channel-client
fomalhautb Oct 13, 2024
69e21b9
added tests for set password, fixed bugs
fomalhautb Oct 13, 2024
84d47de
Merge branch 'dev' into contact-channel-client
fomalhautb Oct 14, 2024
e25179a
added set password
fomalhautb Oct 14, 2024
51602cb
fixed import
fomalhautb Oct 14, 2024
7b38874
updated password section
fomalhautb Oct 14, 2024
6419f51
fixed bugs
fomalhautb Oct 14, 2024
493984d
auto send email verification
fomalhautb Oct 14, 2024
0b10b23
Merge branch 'dev' into contact-channel-client
fomalhautb Oct 14, 2024
a45a001
improved ux
fomalhautb Oct 14, 2024
73e657a
updated ux
fomalhautb Oct 14, 2024
00eb6f7
fixed tests
fomalhautb Oct 14, 2024
1b33669
renamed primaryEmailUsedForAuth back to primaryEmailAuthEnabled
fomalhautb Oct 14, 2024
d286b81
added contact channel sorting
fomalhautb Oct 14, 2024
e6b5fc5
Merge branch 'dev' into contact-channel-client
fomalhautb Oct 19, 2024
e24052a
remoevd unused code
fomalhautb Oct 19, 2024
c23843e
improved create user transaction
fomalhautb Oct 19, 2024
3905e36
merge with dev
fomalhautb Oct 19, 2024
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
283 changes: 142 additions & 141 deletions apps/backend/src/app/api/v1/users/crud.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PrismaTransaction } from "@/lib/types";
import { sendTeamMembershipDeletedWebhook, sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks";
import { prismaClient } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { BooleanTrue, Prisma } from "@prisma/client";
import { BooleanTrue, Prisma, ProjectUser } from "@prisma/client";
import { KnownErrors } from "@stackframe/stack-shared";
import { currentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user";
import { UsersCrud, usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
Expand All @@ -16,6 +16,7 @@ import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies";
import { typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings";
import { waitUntil } from '@vercel/functions';
import { teamPrismaToCrud, teamsCrudHandlers } from "../teams/crud";
import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";

export const userFullInclude = {
projectUserOAuthAccounts: {
Expand Down Expand Up @@ -270,178 +271,178 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC
};
},
onCreate: async ({ auth, data }) => {
const result = await prismaClient.$transaction(async (tx) => {
await checkAuthData(tx, {
const [authMethodConfigs, connectedAccountConfigs, passwordConfig, otpConfig, _] = await Promise.all([
prismaClient.authMethodConfig.findMany({
where: {
projectConfigId: auth.project.config.id,
oauthProviderConfig: {
isNot: null,
}
},
include: {
oauthProviderConfig: true,
}
}),
prismaClient.connectedAccountConfig.findMany({
where: {
projectConfigId: auth.project.config.id,
oauthProviderConfig: {
isNot: null,
}
},
include: {
oauthProviderConfig: true,
}
}),
getPasswordConfig(prismaClient, auth.project.config.id),
getOtpConfig(prismaClient, auth.project.config.id),
checkAuthData(prismaClient, {
projectId: auth.project.id,
primaryEmail: data.primary_email,
primaryEmailVerified: data.primary_email_verified,
primaryEmailAuthEnabled: data.primary_email_auth_enabled,
passwordHash: data.password && await hashPassword(data.password),
});

const newUser = await tx.projectUser.create({
data: {
projectId: auth.project.id,
displayName: data.display_name === undefined ? undefined : (data.display_name || null),
clientMetadata: data.client_metadata === null ? Prisma.JsonNull : data.client_metadata,
clientReadOnlyMetadata: data.client_read_only_metadata === null ? Prisma.JsonNull : data.client_read_only_metadata,
serverMetadata: data.server_metadata === null ? Prisma.JsonNull : data.server_metadata,
profileImageUrl: data.profile_image_url,
totpSecret: data.totp_secret_base64 == null ? data.totp_secret_base64 : Buffer.from(decodeBase64(data.totp_secret_base64)),
},
include: userFullInclude,
});
})
]);

if (data.oauth_providers) {
// TODO: include this in the project
const authMethodConfigs = await tx.authMethodConfig.findMany({
where: {
projectConfigId: auth.project.config.id,
oauthProviderConfig: {
isNot: null,
}
},
include: {
oauthProviderConfig: true,
}
});
const connectedAccountConfigs = await tx.connectedAccountConfig.findMany({
where: {
projectConfigId: auth.project.config.id,
oauthProviderConfig: {
isNot: null,
}
},
include: {
oauthProviderConfig: true,
}
});
const transactions = [];

// create many does not support nested create, so we have to use loop
for (const provider of data.oauth_providers) {
const connectedAccountConfig = connectedAccountConfigs.find((c) => c.oauthProviderConfig?.id === provider.id);
const authMethodConfig = authMethodConfigs.find((c) => c.oauthProviderConfig?.id === provider.id);
const newProjectUserId = generateUuid();
transactions.push(prismaClient.projectUser.create({
data: {
projectUserId: newProjectUserId,
projectId: auth.project.id,
displayName: data.display_name === undefined ? undefined : (data.display_name || null),
clientMetadata: data.client_metadata === null ? Prisma.JsonNull : data.client_metadata,
clientReadOnlyMetadata: data.client_read_only_metadata === null ? Prisma.JsonNull : data.client_read_only_metadata,
serverMetadata: data.server_metadata === null ? Prisma.JsonNull : data.server_metadata,
profileImageUrl: data.profile_image_url,
totpSecret: data.totp_secret_base64 == null ? data.totp_secret_base64 : Buffer.from(decodeBase64(data.totp_secret_base64)),
},
include: userFullInclude,
}));

let authMethod;
if (authMethodConfig) {
authMethod = await tx.authMethod.create({
data: {
projectId: auth.project.id,
projectUserId: newUser.projectUserId,
projectConfigId: auth.project.config.id,
authMethodConfigId: authMethodConfig.id,
}
});
}
if (data.oauth_providers) {
// create many does not support nested create, so we have to use loop
for (const provider of data.oauth_providers) {
const connectedAccountConfig = connectedAccountConfigs.find((c) => c.oauthProviderConfig?.id === provider.id);
const authMethodConfig = authMethodConfigs.find((c) => c.oauthProviderConfig?.id === provider.id);

await tx.projectUserOAuthAccount.create({
let authMethodId;
if (authMethodConfig) {
authMethodId = generateUuid();
transactions.push(prismaClient.authMethod.create({
data: {
id: authMethodId,
projectId: auth.project.id,
projectUserId: newUser.projectUserId,
projectUserId: newProjectUserId,
projectConfigId: auth.project.config.id,
oauthProviderConfigId: provider.id,
providerAccountId: provider.account_id,
email: provider.email,
...connectedAccountConfig ? {
connectedAccount: {
create: {
connectedAccountConfigId: connectedAccountConfig.id,
projectUserId: newUser.projectUserId,
projectConfigId: auth.project.config.id,
}
}
} : {},
...authMethodConfig ? {
oauthAuthMethod: {
create: {
projectUserId: newUser.projectUserId,
projectConfigId: auth.project.config.id,
authMethodId: authMethod?.id || throwErr("authMethodConfig is set but authMethod is not"),
}
}
} : {},
authMethodConfigId: authMethodConfig.id,
}
});
}));
}

}

if (data.primary_email) {
await tx.contactChannel.create({
transactions.push(prismaClient.projectUserOAuthAccount.create({
data: {
projectUserId: newUser.projectUserId,
projectId: auth.project.id,
type: 'EMAIL' as const,
value: data.primary_email,
isVerified: data.primary_email_verified ?? false,
isPrimary: "TRUE",
usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null,
projectUserId: newProjectUserId,
projectConfigId: auth.project.config.id,
oauthProviderConfigId: provider.id,
providerAccountId: provider.account_id,
email: provider.email,
...connectedAccountConfig ? {
connectedAccount: {
create: {
connectedAccountConfigId: connectedAccountConfig.id,
projectUserId: newProjectUserId,
projectConfigId: auth.project.config.id,
}
}
} : {},
...authMethodConfig ? {
oauthAuthMethod: {
create: {
projectUserId: newProjectUserId,
projectConfigId: auth.project.config.id,
authMethodId: authMethodId || throwErr("authMethodId is not set"),
}
}
} : {},
}
});
}));
}
}

if (data.password) {
const passwordConfig = await getPasswordConfig(tx, auth.project.config.id);

if (!passwordConfig) {
throw new StatusError(StatusError.BadRequest, "Password auth not enabled in the project");
if (data.primary_email) {
transactions.push(prismaClient.contactChannel.create({
data: {
projectUserId: newProjectUserId,
projectId: auth.project.id,
type: 'EMAIL' as const,
value: data.primary_email,
isVerified: data.primary_email_verified ?? false,
isPrimary: "TRUE",
usedForAuth: data.primary_email_auth_enabled ? BooleanTrue.TRUE : null,
}
}));
}

await tx.authMethod.create({
data: {
projectId: auth.project.id,
projectConfigId: auth.project.config.id,
projectUserId: newUser.projectUserId,
authMethodConfigId: passwordConfig.authMethodConfigId,
passwordAuthMethod: {
create: {
passwordHash: await hashPassword(data.password),
projectUserId: newUser.projectUserId,
}
}
}
});
if (data.password) {
if (!passwordConfig) {
throw new StatusError(StatusError.BadRequest, "Password auth not enabled in the project");
}

if (data.otp_auth_enabled) {
const otpConfig = await getOtpConfig(tx, auth.project.config.id);

if (!otpConfig) {
throw new StatusError(StatusError.BadRequest, "OTP auth not enabled in the project");
transactions.push(prismaClient.authMethod.create({
data: {
projectId: auth.project.id,
projectConfigId: auth.project.config.id,
projectUserId: newProjectUserId,
authMethodConfigId: passwordConfig.authMethodConfigId,
passwordAuthMethod: {
create: {
passwordHash: await hashPassword(data.password),
projectUserId: newProjectUserId,
}
}
}
}));
}

await tx.authMethod.create({
data: {
projectId: auth.project.id,
projectConfigId: auth.project.config.id,
projectUserId: newUser.projectUserId,
authMethodConfigId: otpConfig.authMethodConfigId,
otpAuthMethod: {
create: {
projectUserId: newUser.projectUserId,
}
if (data.otp_auth_enabled) {
if (!otpConfig) {
throw new StatusError(StatusError.BadRequest, "OTP auth not enabled in the project");
}

transactions.push(prismaClient.authMethod.create({
data: {
projectId: auth.project.id,
projectConfigId: auth.project.config.id,
projectUserId: newProjectUserId,
authMethodConfigId: otpConfig.authMethodConfigId,
otpAuthMethod: {
create: {
projectUserId: newProjectUserId,
}
}
});
}
}
}));
}

const user = await tx.projectUser.findUnique({
where: {
projectId_projectUserId: {
projectId: auth.project.id,
projectUserId: newUser.projectUserId,
},
transactions.push(prismaClient.projectUser.findUnique({
where: {
projectId_projectUserId: {
projectId: auth.project.id,
projectUserId: newProjectUserId,
},
include: userFullInclude,
});

if (!user) {
throw new StackAssertionError("User was created but not found", newUser);
}
},
include: userFullInclude,
}));

return userPrismaToCrud(user, await getUserLastActiveAtMillis(user.projectUserId, new Date()));
});
const transactionResult = await prismaClient.$transaction(transactions);
const user = transactionResult[transactionResult.length - 1] as Prisma.ProjectUserGetPayload<{ include: typeof userFullInclude }>;
const result = userPrismaToCrud(user, await getUserLastActiveAtMillis(user.projectUserId, new Date()));

// TODO: move this into the same transaction
if (auth.project.config.create_team_on_sign_up) {
await teamsCrudHandlers.adminCreate({
data: {
Expand Down
Loading