1.1.3
This commit is contained in:
160
convex/bans.ts
Normal file
160
convex/bans.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
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 };
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user