Version Bump 1.0.40
All checks were successful
Build and Release / build-and-release (push) Successful in 19m24s
All checks were successful
Build and Release / build-and-release (push) Successful in 19m24s
This commit is contained in:
145
convex/auth.ts
145
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: {
|
||||
|
||||
@@ -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"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user