Files
DiscordClone/convex/roles.ts
Bryan1029384756 ff269ee154 nickname
2026-02-18 10:16:12 -06:00

220 lines
5.5 KiB
TypeScript

import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
import { GenericQueryCtx } from "convex/server";
import { DataModel, Id, Doc } from "./_generated/dataModel";
const PERMISSION_KEYS = [
"manage_channels",
"manage_roles",
"manage_messages",
"create_invite",
"embed_links",
"attach_files",
"move_members",
"mute_members",
"manage_nicknames",
] as const;
export async function getRolesForUser(
ctx: GenericQueryCtx<DataModel>,
userId: Id<"userProfiles">
): Promise<Doc<"roles">[]> {
const assignments = await ctx.db
.query("userRoles")
.withIndex("by_user", (q) => q.eq("userId", userId))
.collect();
const roles = await Promise.all(
assignments.map((ur) => ctx.db.get(ur.roleId))
);
return roles.filter((r): r is Doc<"roles"> => r !== null);
}
// List all roles
export const list = query({
args: {},
returns: v.array(v.any()),
handler: async (ctx) => {
const roles = await ctx.db.query("roles").collect();
return roles.sort((a, b) => (b.position || 0) - (a.position || 0));
},
});
// Create new role
export const create = mutation({
args: {
name: v.optional(v.string()),
color: v.optional(v.string()),
permissions: v.optional(v.any()),
position: v.optional(v.number()),
isHoist: v.optional(v.boolean()),
},
returns: v.any(),
handler: async (ctx, args) => {
const id = await ctx.db.insert("roles", {
name: args.name || "new role",
color: args.color || "#99aab5",
position: args.position || 0,
permissions: args.permissions || {},
isHoist: args.isHoist || false,
});
return await ctx.db.get(id);
},
});
// Update role properties
export const update = mutation({
args: {
id: v.id("roles"),
name: v.optional(v.string()),
color: v.optional(v.string()),
permissions: v.optional(v.any()),
position: v.optional(v.number()),
isHoist: v.optional(v.boolean()),
},
returns: v.any(),
handler: async (ctx, args) => {
const role = await ctx.db.get(args.id);
if (!role) throw new Error("Role not found");
const { id, ...fields } = args;
const updates: Record<string, unknown> = {};
for (const [key, value] of Object.entries(fields)) {
if (value !== undefined) updates[key] = value;
}
if (Object.keys(updates).length > 0) {
await ctx.db.patch(id, updates);
}
return await ctx.db.get(id);
},
});
// Delete role
export const remove = mutation({
args: { id: v.id("roles") },
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
const role = await ctx.db.get(args.id);
if (!role) throw new Error("Role not found");
const assignments = await ctx.db
.query("userRoles")
.withIndex("by_role", (q) => q.eq("roleId", args.id))
.collect();
for (const a of assignments) {
await ctx.db.delete(a._id);
}
await ctx.db.delete(args.id);
return { success: true };
},
});
// List members with roles
export const listMembers = query({
args: {},
returns: v.array(v.any()),
handler: async (ctx) => {
const users = await ctx.db.query("userProfiles").collect();
return await Promise.all(
users.map(async (user) => ({
id: user._id,
username: user.username,
public_identity_key: user.publicIdentityKey,
roles: await getRolesForUser(ctx, user._id),
}))
);
},
});
// Assign role to user
export const assign = mutation({
args: {
roleId: v.id("roles"),
userId: v.id("userProfiles"),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("userRoles")
.withIndex("by_user_and_role", (q) =>
q.eq("userId", args.userId).eq("roleId", args.roleId)
)
.unique();
if (!existing) {
await ctx.db.insert("userRoles", {
userId: args.userId,
roleId: args.roleId,
});
}
return { success: true };
},
});
// Remove role from user
export const unassign = mutation({
args: {
roleId: v.id("roles"),
userId: v.id("userProfiles"),
},
returns: v.object({ success: v.boolean() }),
handler: async (ctx, args) => {
const existing = await ctx.db
.query("userRoles")
.withIndex("by_user_and_role", (q) =>
q.eq("userId", args.userId).eq("roleId", args.roleId)
)
.unique();
if (existing) {
await ctx.db.delete(existing._id);
}
return { success: true };
},
});
// Get current user's aggregated permissions
export const getMyPermissions = query({
args: { userId: v.id("userProfiles") },
returns: v.object({
manage_channels: v.boolean(),
manage_roles: v.boolean(),
manage_messages: v.boolean(),
create_invite: v.boolean(),
embed_links: v.boolean(),
attach_files: v.boolean(),
move_members: v.boolean(),
mute_members: v.boolean(),
manage_nicknames: v.boolean(),
}),
handler: async (ctx, args) => {
const roles = await getRolesForUser(ctx, args.userId);
const finalPerms: Record<string, boolean> = {};
for (const key of PERMISSION_KEYS) {
finalPerms[key] = roles.some(
(role) => (role.permissions as Record<string, boolean>)?.[key]
);
}
return finalPerms as {
manage_channels: boolean;
manage_roles: boolean;
manage_messages: boolean;
create_invite: boolean;
embed_links: boolean;
attach_files: boolean;
move_members: boolean;
mute_members: boolean;
manage_nicknames: boolean;
};
},
});