Files
DiscordClone/convex/bans.ts
Bryan1029384756 593eaba82e 1.1.3
2026-04-18 15:41:51 -05:00

161 lines
4.8 KiB
TypeScript

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import type { GenericQueryCtx } from "convex/server";
import type { DataModel, Id } from "./_generated/dataModel";
import { hasPermission, getRolesForUser } from "./roles";
import { AUDIT_ACTIONS, logAudit } from "./audit";
import { getPublicStorageUrl } from "./storageUrl";
/**
* Returns true if the user is currently banned. Callers should
* surface this as an explicit `{ error: "Banned" }` at auth time or
* throw from sensitive mutations like `messages.send`.
*/
export async function isBanned(
ctx: GenericQueryCtx<DataModel>,
userId: Id<"userProfiles">,
): Promise<boolean> {
const row = await ctx.db
.query("bans")
.withIndex("by_user", (q) => q.eq("userId", userId))
.first();
return !!row;
}
export const isUserBanned = query({
args: { userId: v.id("userProfiles") },
returns: v.boolean(),
handler: async (ctx, args) => isBanned(ctx, args.userId),
});
// List all bans with actor + target enrichment for the Bans tab.
export const list = query({
args: { actorId: v.id("userProfiles") },
returns: v.array(v.any()),
handler: async (ctx, args) => {
if (!(await hasPermission(ctx, args.actorId, "ban_members"))) {
throw new Error("You don't have permission to view bans.");
}
const bans = await ctx.db.query("bans").collect();
const out = [];
for (const b of bans) {
const user = await ctx.db.get(b.userId);
const actor = await ctx.db.get(b.bannedBy);
let avatarUrl: string | null = null;
if (user?.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
}
out.push({
_id: b._id,
userId: b.userId,
bannedBy: b.bannedBy,
reason: b.reason ?? null,
createdAt: b.createdAt,
user: user
? {
username: user.username,
displayName: user.displayName,
avatarUrl,
}
: null,
actor: actor
? { username: actor.username, displayName: actor.displayName }
: null,
});
}
out.sort((a, b) => b.createdAt - a.createdAt);
return out;
},
});
export const ban = mutation({
args: {
actorId: v.id("userProfiles"),
userId: v.id("userProfiles"),
reason: v.optional(v.string()),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
if (!(await hasPermission(ctx, args.actorId, "ban_members"))) {
throw new Error("You don't have permission to ban members.");
}
if (args.actorId === args.userId) {
throw new Error("You can't ban yourself.");
}
const target = await ctx.db.get(args.userId);
if (!target) throw new Error("User not found.");
// Refuse to ban isAdmin or Owner-role bearers. Single-server
// deployment can't afford to lock itself out of administration.
if (target.isAdmin) {
throw new Error("Server admins can't be banned.");
}
const targetRoles = await getRolesForUser(ctx, args.userId);
if (targetRoles.some((r) => r.name === "Owner")) {
throw new Error("The Owner can't be banned.");
}
const existing = await ctx.db
.query("bans")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.first();
if (existing) {
// Update reason + actor if re-banning.
await ctx.db.patch(existing._id, {
bannedBy: args.actorId,
reason: args.reason,
createdAt: Date.now(),
});
} else {
await ctx.db.insert("bans", {
userId: args.userId,
bannedBy: args.actorId,
reason: args.reason,
createdAt: Date.now(),
});
}
await logAudit(ctx, {
actorId: args.actorId,
action: AUDIT_ACTIONS.BAN_ADD,
targetType: "user",
targetId: args.userId,
targetName: target.displayName ?? target.username,
metadata: args.reason ? { reason: args.reason } : undefined,
});
return { success: true };
},
});
export const unban = mutation({
args: {
actorId: v.id("userProfiles"),
userId: v.id("userProfiles"),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
if (!(await hasPermission(ctx, args.actorId, "ban_members"))) {
throw new Error("You don't have permission to unban members.");
}
const row = await ctx.db
.query("bans")
.withIndex("by_user", (q) => q.eq("userId", args.userId))
.first();
if (!row) return { success: true };
const target = await ctx.db.get(args.userId);
await ctx.db.delete(row._id);
await logAudit(ctx, {
actorId: args.actorId,
action: AUDIT_ACTIONS.BAN_REMOVE,
targetType: "user",
targetId: args.userId,
targetName: target?.displayName ?? target?.username,
});
return { success: true };
},
});