feat: Add initial Discord clone application with Convex backend services and Electron React frontend components.

This commit is contained in:
Bryan1029384756
2026-02-10 05:27:10 -06:00
parent 47f173c79b
commit 34e9790db9
29 changed files with 3254 additions and 1398 deletions

View File

@@ -1,6 +1,16 @@
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
async function sha256Hex(input: string): Promise<string> {
const buffer = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(input)
);
return Array.from(new Uint8Array(buffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// Get salt for a username (returns fake salt for non-existent users)
export const getSalt = query({
args: { username: v.string() },
@@ -16,15 +26,7 @@ export const getSalt = query({
}
// 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("");
const fakeSalt = await sha256Hex("SERVER_SECRET_KEY" + args.username);
return { salt: fakeSalt };
},
});
@@ -55,14 +57,7 @@ export const verifyUser = mutation({
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("");
const hashedDAK = await sha256Hex(args.dak);
if (hashedDAK === user.hashedAuthKey) {
return {
@@ -95,7 +90,6 @@ export const createUserWithProfile = mutation({
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))
@@ -105,17 +99,14 @@ export const createUserWithProfile = mutation({
return { error: "Username taken" };
}
// Count existing users
const allUsers = await ctx.db.query("userProfiles").collect();
const userCount = allUsers.length;
const isFirstUser =
(await ctx.db.query("userProfiles").first()) === null;
// Enforce invite code for non-first users
if (userCount > 0) {
if (!isFirstUser) {
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))
@@ -137,11 +128,9 @@ export const createUserWithProfile = mutation({
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,
@@ -150,12 +139,10 @@ export const createUserWithProfile = mutation({
publicIdentityKey: args.publicKey,
publicSigningKey: args.signingKey,
encryptedPrivateKeys: args.encryptedPrivateKeys,
isAdmin: userCount === 0,
isAdmin: isFirstUser,
});
// First user bootstrap: create Owner + @everyone roles if they don't exist
if (userCount === 0) {
// Create @everyone role
if (isFirstUser) {
const everyoneRoleId = await ctx.db.insert("roles", {
name: "@everyone",
color: "#99aab5",
@@ -168,7 +155,6 @@ export const createUserWithProfile = mutation({
isHoist: false,
});
// Create Owner role
const ownerRoleId = await ctx.db.insert("roles", {
name: "Owner",
color: "#e91e63",
@@ -183,11 +169,9 @@ export const createUserWithProfile = mutation({
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"))