From 9ef839938e7de23f87667db8901cb19aadaae2b4 Mon Sep 17 00:00:00 2001 From: Bryan1029384756 <23323626+Bryan1029384756@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:47:40 -0500 Subject: [PATCH] Version Bump 1.0.40 --- apps/android/android/app/build.gradle | 2 +- apps/android/package.json | 2 +- apps/electron/package.json | 2 +- apps/web/package.json | 2 +- convex/auth.ts | 145 ++++++++++++++++++ convex/schema.ts | 16 +- packages/shared/package.json | 2 +- .../src/components/ServerSettingsModal.jsx | 78 +++++++--- packages/shared/src/components/Sidebar.jsx | 21 ++- 9 files changed, 229 insertions(+), 41 deletions(-) diff --git a/apps/android/android/app/build.gradle b/apps/android/android/app/build.gradle index 635004f..c31c338 100644 --- a/apps/android/android/app/build.gradle +++ b/apps/android/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 27 - versionName "1.0.39" + versionName "1.0.40" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/apps/android/package.json b/apps/android/package.json index ce89637..ad427d1 100644 --- a/apps/android/package.json +++ b/apps/android/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/android", "private": true, - "version": "1.0.39", + "version": "1.0.40", "type": "module", "scripts": { "cap:sync": "npx cap sync", diff --git a/apps/electron/package.json b/apps/electron/package.json index e085028..4f8850c 100644 --- a/apps/electron/package.json +++ b/apps/electron/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/electron", "private": true, - "version": "1.0.39", + "version": "1.0.40", "description": "Brycord - Electron app", "author": "Moyettes", "type": "module", diff --git a/apps/web/package.json b/apps/web/package.json index 317e049..f96ee7f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/web", "private": true, - "version": "1.0.39", + "version": "1.0.40", "type": "module", "scripts": { "dev": "vite", diff --git a/convex/auth.ts b/convex/auth.ts index 398b075..984e730 100644 --- a/convex/auth.ts +++ b/convex/auth.ts @@ -367,6 +367,151 @@ export const setNickname = mutation({ }, }); +// Delete a user and all their associated data (admin only) +export const deleteUser = mutation({ + args: { + requestingUserId: v.id("userProfiles"), + targetUserId: v.id("userProfiles"), + }, + returns: v.object({ success: v.boolean(), error: v.optional(v.string()) }), + handler: async (ctx, args) => { + // Verify requesting user is admin + const requester = await ctx.db.get(args.requestingUserId); + if (!requester || !requester.isAdmin) { + return { success: false, error: "Only admins can delete users" }; + } + + // Prevent self-deletion + if (args.requestingUserId === args.targetUserId) { + return { success: false, error: "Cannot delete your own account" }; + } + + const target = await ctx.db.get(args.targetUserId); + if (!target) { + return { success: false, error: "User not found" }; + } + + // Prevent deleting other admins + if (target.isAdmin) { + return { success: false, error: "Cannot delete another admin" }; + } + + // Delete reactions made by this user (before messages, using index) + const userReactions = await ctx.db + .query("messageReactions") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const r of userReactions) { + await ctx.db.delete(r._id); + } + + // Delete all messages by this user (using index) + const messages = await ctx.db + .query("messages") + .withIndex("by_sender", (q) => q.eq("senderId", args.targetUserId)) + .collect(); + for (const msg of messages) { + // Delete reactions on this message + const reactions = await ctx.db + .query("messageReactions") + .withIndex("by_message", (q) => q.eq("messageId", msg._id)) + .collect(); + for (const r of reactions) { + await ctx.db.delete(r._id); + } + await ctx.db.delete(msg._id); + } + + // Delete channel keys + const channelKeys = await ctx.db + .query("channelKeys") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const ck of channelKeys) { + await ctx.db.delete(ck._id); + } + + // Delete role assignments + const userRoles = await ctx.db + .query("userRoles") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const ur of userRoles) { + await ctx.db.delete(ur._id); + } + + // Delete DM participations + const dmParts = await ctx.db + .query("dmParticipants") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const dp of dmParts) { + await ctx.db.delete(dp._id); + } + + // Delete typing indicators + const typingIndicators = await ctx.db + .query("typingIndicators") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const ti of typingIndicators) { + await ctx.db.delete(ti._id); + } + + // Delete voice states + const voiceStates = await ctx.db + .query("voiceStates") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const vs of voiceStates) { + await ctx.db.delete(vs._id); + } + + // Delete read states + const readStates = await ctx.db + .query("channelReadState") + .withIndex("by_user", (q) => q.eq("userId", args.targetUserId)) + .collect(); + for (const rs of readStates) { + await ctx.db.delete(rs._id); + } + + // Delete invites created by this user + const invites = await ctx.db + .query("invites") + .withIndex("by_creator", (q) => q.eq("createdBy", args.targetUserId)) + .collect(); + for (const inv of invites) { + await ctx.db.delete(inv._id); + } + + // Delete custom emojis uploaded by this user + const emojis = await ctx.db + .query("customEmojis") + .withIndex("by_uploader", (q) => q.eq("uploadedBy", args.targetUserId)) + .collect(); + for (const emoji of emojis) { + await ctx.storage.delete(emoji.storageId); + await ctx.db.delete(emoji._id); + } + + // Delete avatar from storage if exists + if (target.avatarStorageId) { + await ctx.storage.delete(target.avatarStorageId); + } + + // Delete join sound from storage if exists + if (target.joinSoundStorageId) { + await ctx.storage.delete(target.joinSoundStorageId); + } + + // Delete the user profile + await ctx.db.delete(args.targetUserId); + + return { success: true }; + }, +}); + // Internal: update credentials after password reset export const updateCredentials = internalMutation({ args: { diff --git a/convex/schema.ts b/convex/schema.ts index 8f86ccc..8e56af8 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -44,7 +44,8 @@ export default defineSchema({ editedAt: v.optional(v.number()), pinned: v.optional(v.boolean()), }).index("by_channel", ["channelId"]) - .index("by_channel_pinned", ["channelId", "pinned"]), + .index("by_channel_pinned", ["channelId", "pinned"]) + .index("by_sender", ["senderId"]), messageReactions: defineTable({ messageId: v.id("messages"), @@ -52,7 +53,8 @@ export default defineSchema({ emoji: v.string(), }) .index("by_message", ["messageId"]) - .index("by_message_user_emoji", ["messageId", "userId", "emoji"]), + .index("by_message_user_emoji", ["messageId", "userId", "emoji"]) + .index("by_user", ["userId"]), channelKeys: defineTable({ channelId: v.id("channels"), @@ -88,7 +90,8 @@ export default defineSchema({ uses: v.number(), expiresAt: v.optional(v.number()), // timestamp keyVersion: v.number(), - }).index("by_code", ["code"]), + }).index("by_code", ["code"]) + .index("by_creator", ["createdBy"]), dmParticipants: defineTable({ channelId: v.id("channels"), @@ -102,7 +105,8 @@ export default defineSchema({ userId: v.id("userProfiles"), username: v.string(), expiresAt: v.number(), // timestamp - }).index("by_channel", ["channelId"]), + }).index("by_channel", ["channelId"]) + .index("by_user", ["userId"]), voiceStates: defineTable({ channelId: v.id("channels"), @@ -138,6 +142,8 @@ export default defineSchema({ name: v.string(), storageId: v.id("_storage"), uploadedBy: v.id("userProfiles"), + createdAt: v.number(), - }).index("by_name", ["name"]), + }).index("by_name", ["name"]) + .index("by_uploader", ["uploadedBy"]), }); diff --git a/packages/shared/package.json b/packages/shared/package.json index e905dc5..59e9b8c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,7 +1,7 @@ { "name": "@discord-clone/shared", "private": true, - "version": "1.0.39", + "version": "1.0.40", "type": "module", "main": "src/App.jsx", "dependencies": { diff --git a/packages/shared/src/components/ServerSettingsModal.jsx b/packages/shared/src/components/ServerSettingsModal.jsx index c163551..7e29852 100644 --- a/packages/shared/src/components/ServerSettingsModal.jsx +++ b/packages/shared/src/components/ServerSettingsModal.jsx @@ -493,27 +493,47 @@ const ServerSettingsModal = ({ onClose }) => { ); + const isCurrentUserAdmin = members.find(m => m.id === userId)?.roles?.some(r => r.name === 'Owner'); + + const handleDeleteUser = async (targetUserId, targetUsername) => { + if (!confirm(`Are you sure you want to delete "${targetUsername}" and ALL their messages? This cannot be undone.`)) return; + try { + const result = await convex.mutation(api.auth.deleteUser, { + requestingUserId: userId, + targetUserId, + }); + if (!result.success) { + alert(result.error || 'Failed to delete user.'); + } + } catch (e) { + console.error('Delete user error:', e); + alert('Failed to delete user. See console.'); + } + }; + const renderMembersTab = () => (