oscklm
oscklm11mo ago

Unsure about right pattern when following zen of convex for actions

I'm trying to follow the concepts from the zen of convex. In this case i wanna create a new user from the client. I go ahead and make a mutation, that i can call from the client, like this example below:
export const createUser = internalMutation({
args: {
username: v.string(),
password: v.string(),
email: v.optional(v.string()),
},
handler: async (ctx, { username, password, email }) => {

// Do we create the user already here?? we dont have hashed password yet

// Run action that will schedule our action handling hashing the password etc.
// ctx.scheduler.runAfter(...);
},
});
export const createUser = internalMutation({
args: {
username: v.string(),
password: v.string(),
email: v.optional(v.string()),
},
handler: async (ctx, { username, password, email }) => {

// Do we create the user already here?? we dont have hashed password yet

// Run action that will schedule our action handling hashing the password etc.
// ctx.scheduler.runAfter(...);
},
});
Am i on the wrong path here, i'm assuming it's best to create my hashed password using a action running on the server, but finding it a bit hard to follow zen of convex here.
14 Replies
ballingt
ballingt11mo ago
I think the zen-of-convex way to do this would be to do it all in the mutation; hashing a password is just compute, not interacting with an external system. So you shouldn't have to break out into a scheduled action. Is this because the Convex runtime is missing some hashing functionality you need?
oscklm
oscklmOP11mo ago
Deleted previous reply, as i got confused. So i basically cannot do this in a normal mutation, which is why i'd wanna use an action:
export const createUser = mutation({
args: {
username: v.string(),
password: v.string(),
email: v.optional(v.string()),
},
handler: async (ctx, { username, password, email }) => {
// Generate a salt
const salt = randomBytes(16).toString("hex");
// Hash the password using the salt
const hashedBuffer = await scryptAsync(password, salt);
const hashedPassword = `${salt}.${hashedBuffer.toString("hex")}`;

// Create a new user object
const newUser = await ctx.db.insert("user", {
username,
hashedPassword,
email,
});
},
});

async function scryptAsync(password: string, salt: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
scrypt(password, salt, 64, (err, derivedKey: Buffer) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
}
export const createUser = mutation({
args: {
username: v.string(),
password: v.string(),
email: v.optional(v.string()),
},
handler: async (ctx, { username, password, email }) => {
// Generate a salt
const salt = randomBytes(16).toString("hex");
// Hash the password using the salt
const hashedBuffer = await scryptAsync(password, salt);
const hashedPassword = `${salt}.${hashedBuffer.toString("hex")}`;

// Create a new user object
const newUser = await ctx.db.insert("user", {
username,
hashedPassword,
email,
});
},
});

async function scryptAsync(password: string, salt: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
scrypt(password, salt, 64, (err, derivedKey: Buffer) => {
if (err) reject(err);
else resolve(derivedKey);
});
});
}
oscklm
oscklmOP11mo ago
No description
ballingt
ballingt11mo ago
Yeah looks like scrypt is not provided by WebCrypto, the crypto that is build into the Convex runtime https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API You could use https://www.npmjs.com/package/scrypt-js I found this helpful reading https://gist.github.com/chrisveness/770ee96945ec12ac84f134bf538d89fb
oscklm
oscklmOP11mo ago
Yeah. So basically to give a little context here, i'm just trying to learn about authentication - trying to build a small authentication system with pure convex. I know lucia auth provides some packages, and helpers for all of this. But i'd like to see how far i can get by just using convex and whats available to me.
ballingt
ballingt11mo ago
as in using no npm libraries? cool!
oscklm
oscklmOP11mo ago
Awesome, thanks for linking. I'll take a look at that!
ballingt
ballingt11mo ago
IF the goal is just to hash a password https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest looks good
oscklm
oscklmOP11mo ago
Yeah exactly. I mean my whole point of wanting to learn about it, is that i eventually wanna try and cut clerk (although i love what they provide) but i mean i'm very curious about auth so i wanna see how far i can get on my own, and i feel like that's also where im gonna learn most, since it 4 sure aint easy stuff atleast very different from my day to day
ballingt
ballingt11mo ago
seems to work
No description
oscklm
oscklmOP11mo ago
Yeah, then my next step is authenticating the user, and this is what i've gotten so far:
export const authenticateUser = internalAction({
args: { username: v.string(), password: v.string() },
handler: async (ctx, { username, password }) => {
const user = await ctx.runQuery(internal.user.getByUsername, {
username,
});

if (!user) {
throw new Error("User not found");
}

const isPasswordValid = await verifyPassword(password, user.hashedPassword);

if (user && isPasswordValid) {
const token: string = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET!,
{
expiresIn: "24h",
}
);

// Calculate expiration date for the session, e.g., 24 hours from now
const expiresAt = Date.now() + 24 * 60 * 60 * 1000;

// Create a new session in the database
const sessionId: Id<"session"> = await ctx.runMutation(
internal.sessions.createSession,
{
userId: user._id,
expiresAt: expiresAt,
}
);

return { token, sessionId };
} else {
throw new Error("Authentication failed");
}
},
});
export const authenticateUser = internalAction({
args: { username: v.string(), password: v.string() },
handler: async (ctx, { username, password }) => {
const user = await ctx.runQuery(internal.user.getByUsername, {
username,
});

if (!user) {
throw new Error("User not found");
}

const isPasswordValid = await verifyPassword(password, user.hashedPassword);

if (user && isPasswordValid) {
const token: string = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET!,
{
expiresIn: "24h",
}
);

// Calculate expiration date for the session, e.g., 24 hours from now
const expiresAt = Date.now() + 24 * 60 * 60 * 1000;

// Create a new session in the database
const sessionId: Id<"session"> = await ctx.runMutation(
internal.sessions.createSession,
{
userId: user._id,
expiresAt: expiresAt,
}
);

return { token, sessionId };
} else {
throw new Error("Authentication failed");
}
},
});
Still very early on with this project, but i'm keeping it 100% open source here, hopefully at some point reaching something that can be used. Then comes all the security improvements haha, but one step at a time
oscklm
oscklmOP11mo ago
GitHub
GitHub - oscdot/convex-auth
Contribute to oscdot/convex-auth development by creating an account on GitHub.
ballingt
ballingt11mo ago
awesome!
oscklm
oscklmOP11mo ago
I'm glad you say that. It's fun so far, learning a lot of stuff Thats awesome! looking thorugh the links u sent

Did you find this page helpful?