208 lines
5.1 KiB
TypeScript
208 lines
5.1 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",
|
|
"create_invite",
|
|
"embed_links",
|
|
"attach_files",
|
|
] as const;
|
|
|
|
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(),
|
|
create_invite: v.boolean(),
|
|
embed_links: v.boolean(),
|
|
attach_files: 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;
|
|
create_invite: boolean;
|
|
embed_links: boolean;
|
|
attach_files: boolean;
|
|
};
|
|
},
|
|
});
|