Skip to content
Draft
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
2 changes: 1 addition & 1 deletion convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const schema = defineEntSchema({
.field("clerkId", v.string(), { unique: true })
.field("username", v.string(), { unique: true })
.field("firstName", v.optional(v.string()))
.field("email", v.optional(v.string()))
.field("email", v.optional(v.string()), { unique: true })
.field("lastName", v.optional(v.string()))
.edges("reactions", { ref: true })
.edges("privateChats")
Expand Down
178 changes: 170 additions & 8 deletions convex/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { createClerkClient } from "@clerk/backend";
import { isClerkAPIResponseError } from "@clerk/shared";
import { ActionRetrier } from "@convex-dev/action-retrier";
import { v } from "convex/values";
import {
formSchemaUserUpdate,
FormSchemaUserUpdate,
} from "../src/lib/validators";
import { internal } from "./_generated/api";
import { action, internalAction, internalMutation } from "./_generated/server";
import { mutation, query } from "./lib/functions";

export const getUserData = query({
Expand All @@ -14,23 +23,26 @@ export const getUserData = query({
},
});

export const updateUserData = mutation({
export const updateUserDataConvex = internalMutation({
args: {
data: v.object({
firstName: v.optional(v.string()),
lastName: v.optional(v.string()),
email: v.optional(v.string()),
}),
identity: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
if (identity === null) {
console.error("Unauthenticated call to mutation");
const user = await ctx.db
.query("users")
.filter((q) => q.eq(q.field("clerkId"), args.identity))
.unique();

if (user == null) {
console.error("User not found");
return null;
}

const user = ctx.table("users").getX("clerkId", identity.tokenIdentifier);

const updates: { email?: string; lastName?: string; firstName?: string } =
{};
if (args.data.email) {
Expand All @@ -43,7 +55,157 @@ export const updateUserData = mutation({
updates.firstName = args.data.firstName;
}

// Use one patch instead of a few singular patches for better performance
await user.patch(updates);
await ctx.db.patch(user._id, updates);
},
});

export const updateClerkPassword = internalAction({
args: {
password: v.string(),
identity: v.string(),
},
handler: async (ctx, args) => {
const userId = args.identity.split("|")[1];

if (!userId) {
throw new Error("Invalid user ID");
}

const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
});

console.log(userId, args.password);

try {
await clerkClient.users.updateUser(userId, {
password: "test",
});
} catch (e) {
console.error(e);
}
},
});

export const updatePassword = mutation({
args: {
password: v.string(),
},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();

if (identity === null) {
console.error("Unauthenticated call to mutation");
return null;
}

await ctx.scheduler.runAfter(0, internal.users.updateClerkPassword, {
identity: identity.tokenIdentifier,
password: args.password,
});
},
});

export const getIdentity = internalMutation({
args: {},
handler: async (ctx, args) => {
const identity = await ctx.auth.getUserIdentity();
return identity?.tokenIdentifier;
},
});

export const updateUserData = action({
args: {
data: v.object({
firstName: v.optional(v.string()),
lastName: v.optional(v.string()),
email: v.optional(v.string()),
}),
},
handler: async (ctx, args) => {
const unparsedSignUpHeaders = args.data satisfies FormSchemaUserUpdate;
const parsedSignUpHeaders = formSchemaUserUpdate.safeParse(
unparsedSignUpHeaders,
);

const identity = await ctx.runMutation(internal.users.getIdentity);
if (identity === null) {
console.error("Unauthenticated call to mutation");
return null;
}
const userId = identity.split("|")[1];

if (!parsedSignUpHeaders.success) {
return console.error("You have to provide valid data");
} else {
// Use one patch instead of a few singular patches for better performance

try {
const clerkClient = createClerkClient({
secretKey: process.env.CLERK_SECRET_KEY,
});

await clerkClient.users.updateUser(userId, {
firstName: args.data.firstName,
lastName: args.data.lastName,
});

await ctx.runMutation(internal.users.updateUserDataConvex, {
identity: identity,
data: {
firstName: args.data.firstName,
lastName: args.data.lastName,
},
});

if (args.data.email) {
try {
await clerkClient.emailAddresses.createEmailAddress({
userId: userId,
emailAddress: args.data.email,
verified: true,
primary: false,
});

await ctx.runMutation(internal.users.updateUserDataConvex, {
identity: identity,
data: {
email: args.data.email,
},
});

const userData = await clerkClient.users.getUser(userId);

if (userData.emailAddresses[1] !== undefined) {
await clerkClient.emailAddresses.deleteEmailAddress(
userData.emailAddresses[1].id,
);
}
} catch (e) {
console.error(e);
if (isClerkAPIResponseError(e)) {
if (
e.errors.some(
(error) => error.code === "form_param_format_invalid",
)
) {
throw new Error("form_param_format_invalid");
}
if (
e.errors.some(
(error) => error.code === "form_identifier_exists",
)
) {
throw new Error("form_identifier_exists");
}
}
}
}
} catch (e) {
if (e instanceof Error) {
return e.message;
}
}
}
},
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.4",
"@reactuses/core": "^5.0.23",
"@sentry/nextjs": "^8",
"@sentry/nextjs": "^8.42.0",
"@serwist/next": "9.0.11",
"@t3-oss/env-nextjs": "^0.11.1",
"babel-plugin-react-compiler": "19.0.0-beta-df7b47d-20241124",
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading