import { query, mutation } from "./_generated/server"; import { v } from "convex/values"; // Get salt for a username (returns fake salt for non-existent users) export const getSalt = query({ args: { username: v.string() }, returns: v.object({ salt: v.string() }), handler: async (ctx, args) => { const user = await ctx.db .query("userProfiles") .withIndex("by_username", (q) => q.eq("username", args.username)) .unique(); if (user) { return { salt: user.clientSalt }; } // Generate deterministic fake salt for non-existent users (privacy) // Simple HMAC-like approach using username const encoder = new TextEncoder(); const data = encoder.encode("SERVER_SECRET_KEY" + args.username); const hashBuffer = await crypto.subtle.digest("SHA-256", data); const hashArray = new Uint8Array(hashBuffer); const fakeSalt = Array.from(hashArray) .map((b) => b.toString(16).padStart(2, "0")) .join(""); return { salt: fakeSalt }; }, }); // Verify user credentials (DAK comparison) export const verifyUser = mutation({ args: { username: v.string(), dak: v.string(), }, returns: v.union( v.object({ success: v.boolean(), userId: v.string(), encryptedMK: v.string(), encryptedPrivateKeys: v.string(), publicKey: v.string(), }), v.object({ error: v.string() }) ), handler: async (ctx, args) => { const user = await ctx.db .query("userProfiles") .withIndex("by_username", (q) => q.eq("username", args.username)) .unique(); if (!user) { return { error: "Invalid credentials" }; } // Hash the DAK with SHA-256 and compare const encoder = new TextEncoder(); const dakBuffer = encoder.encode(args.dak); const hashBuffer = await crypto.subtle.digest("SHA-256", dakBuffer); const hashArray = new Uint8Array(hashBuffer); const hashedDAK = Array.from(hashArray) .map((b) => b.toString(16).padStart(2, "0")) .join(""); if (hashedDAK === user.hashedAuthKey) { return { success: true, userId: user._id, encryptedMK: user.encryptedMasterKey, encryptedPrivateKeys: user.encryptedPrivateKeys, publicKey: user.publicIdentityKey, }; } return { error: "Invalid credentials" }; }, }); // Register new user with crypto keys export const createUserWithProfile = mutation({ args: { username: v.string(), salt: v.string(), encryptedMK: v.string(), hak: v.string(), publicKey: v.string(), signingKey: v.string(), encryptedPrivateKeys: v.string(), inviteCode: v.optional(v.string()), }, returns: v.union( v.object({ success: v.boolean(), userId: v.string() }), v.object({ error: v.string() }) ), handler: async (ctx, args) => { // Check if username is taken const existing = await ctx.db .query("userProfiles") .withIndex("by_username", (q) => q.eq("username", args.username)) .unique(); if (existing) { return { error: "Username taken" }; } // Count existing users const allUsers = await ctx.db.query("userProfiles").collect(); const userCount = allUsers.length; // Enforce invite code for non-first users if (userCount > 0) { if (!args.inviteCode) { return { error: "Invite code required" }; } // Validate invite const invite = await ctx.db .query("invites") .withIndex("by_code", (q) => q.eq("code", args.inviteCode)) .unique(); if (!invite) { return { error: "Invalid invite code" }; } if (invite.expiresAt && Date.now() > invite.expiresAt) { return { error: "Invite expired" }; } if ( invite.maxUses !== undefined && invite.maxUses !== null && invite.uses >= invite.maxUses ) { return { error: "Invite max uses reached" }; } // Increment invite usage await ctx.db.patch(invite._id, { uses: invite.uses + 1 }); } // Create user profile const userId = await ctx.db.insert("userProfiles", { username: args.username, clientSalt: args.salt, encryptedMasterKey: args.encryptedMK, hashedAuthKey: args.hak, publicIdentityKey: args.publicKey, publicSigningKey: args.signingKey, encryptedPrivateKeys: args.encryptedPrivateKeys, isAdmin: userCount === 0, }); // First user bootstrap: create Owner + @everyone roles if they don't exist if (userCount === 0) { // Create @everyone role const everyoneRoleId = await ctx.db.insert("roles", { name: "@everyone", color: "#99aab5", position: 0, permissions: { create_invite: true, embed_links: true, attach_files: true, }, isHoist: false, }); // Create Owner role const ownerRoleId = await ctx.db.insert("roles", { name: "Owner", color: "#e91e63", position: 100, permissions: { manage_channels: true, manage_roles: true, create_invite: true, embed_links: true, attach_files: true, }, isHoist: true, }); // Assign both roles to first user await ctx.db.insert("userRoles", { userId, roleId: everyoneRoleId }); await ctx.db.insert("userRoles", { userId, roleId: ownerRoleId }); } else { // Assign @everyone role to new user const everyoneRole = await ctx.db .query("roles") .filter((q) => q.eq(q.field("name"), "@everyone")) .first(); if (everyoneRole) { await ctx.db.insert("userRoles", { userId, roleId: everyoneRole._id, }); } } return { success: true, userId }; }, }); // Get all users' public keys export const getPublicKeys = query({ args: {}, returns: v.array( v.object({ id: v.string(), username: v.string(), public_identity_key: v.string(), }) ), handler: async (ctx) => { const users = await ctx.db.query("userProfiles").collect(); return users.map((u) => ({ id: u._id, username: u.username, public_identity_key: u.publicIdentityKey, })); }, });