This commit is contained in:
Bryan1029384756
2026-04-18 15:41:51 -05:00
parent 938df217f4
commit 593eaba82e
47 changed files with 3539 additions and 212 deletions

View File

@@ -13,6 +13,7 @@ const PERMISSION_KEYS = [
"move_members",
"mute_members",
"manage_nicknames",
"ban_members",
] as const;
export async function getRolesForUser(
@@ -30,6 +31,31 @@ export async function getRolesForUser(
return roles.filter((r): r is Doc<"roles"> => r !== null);
}
/**
* Server-side permission check. Use before any mutation that's
* supposed to be gated — the client-side `getMyPermissions` hides
* UI, but a crafted client can still call the mutation.
*
* Treats `isAdmin` bootstrap flag and the "Owner" role as granting
* every permission, including ones that don't yet exist on the role
* row. That keeps future permission additions working for the
* original server owner without requiring a migration pass.
*/
export async function hasPermission(
ctx: GenericQueryCtx<DataModel>,
userId: Id<"userProfiles">,
key: (typeof PERMISSION_KEYS)[number],
): Promise<boolean> {
const user = await ctx.db.get(userId);
if (!user) return false;
if (user.isAdmin) return true;
const roles = await getRolesForUser(ctx, userId);
if (roles.some((r) => r.name === "Owner")) return true;
return roles.some(
(r) => (r.permissions as Record<string, boolean> | undefined)?.[key] === true,
);
}
// List all roles
export const list = query({
args: {},
@@ -249,15 +275,22 @@ export const getMyPermissions = query({
move_members: v.boolean(),
mute_members: v.boolean(),
manage_nicknames: v.boolean(),
ban_members: v.boolean(),
}),
handler: async (ctx, args) => {
const user = await ctx.db.get(args.userId);
const roles = await getRolesForUser(ctx, args.userId);
// isAdmin or Owner-role bearers get everything — same logic as
// `hasPermission`. Keeps UI and server enforcement consistent.
const isSuper = !!user?.isAdmin || roles.some((r) => r.name === "Owner");
const finalPerms: Record<string, boolean> = {};
for (const key of PERMISSION_KEYS) {
finalPerms[key] = roles.some(
(role) => (role.permissions as Record<string, boolean>)?.[key]
);
finalPerms[key] =
isSuper ||
roles.some(
(role) => (role.permissions as Record<string, boolean>)?.[key],
);
}
return finalPerms as {
@@ -270,6 +303,7 @@ export const getMyPermissions = query({
move_members: boolean;
mute_members: boolean;
manage_nicknames: boolean;
ban_members: boolean;
};
},
});