-
- {totalCount - items.length} more {type}...
-
+
+
+ {icon}
+
+ {totalCount} {getDisplayText(totalCount, kind)}
- )}
+
);
};
+
+const getDisplayText = (count: number, kind: "keys" | "permissions") => {
+ if (count === 1) {
+ return kind === "keys" ? "Key" : "Permission";
+ }
+ return kind === "keys" ? "Keys" : "Permissions";
+};
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/table/components/skeletons.tsx b/apps/dashboard/app/(app)/authorization/roles/components/table/components/skeletons.tsx
index 99e8b8a504..2358997eb1 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/table/components/skeletons.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/components/table/components/skeletons.tsx
@@ -20,28 +20,20 @@ export const SlugColumnSkeleton = () => (
);
export const AssignedKeysColumnSkeleton = () => (
-
+
);
export const PermissionsColumnSkeleton = () => (
-
+
);
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-role-limits.ts b/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-role-limits.ts
new file mode 100644
index 0000000000..81e252d924
--- /dev/null
+++ b/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-role-limits.ts
@@ -0,0 +1,90 @@
+import { trpc } from "@/lib/trpc/client";
+
+export type RoleLimitState = {
+ totalKeys: number;
+ totalPerms: number;
+ hasKeyWarning: boolean;
+ hasPermWarning: boolean;
+ shouldPrefetch: boolean;
+ shouldAllowEdit: boolean;
+};
+
+// `MAX_ATTACH_LIMIT` threshold for role attachments. Beyond this limit:
+// - Role editing is disabled to prevent UI performance degradation
+// - Warning callouts are shown to inform users of potential slowdowns
+// - Prefetching of connected keys/permissions is skipped to reduce API load
+export const MAX_ATTACH_LIMIT = 50;
+
+export const useRoleLimits = (roleId?: string) => {
+ const trpcUtils = trpc.useUtils();
+
+ const getKeysPreview = () => {
+ if (!roleId) {
+ return null;
+ }
+ return trpcUtils.authorization.roles.connectedKeys.getData({
+ roleId,
+ });
+ };
+
+ const getPermsPreview = () => {
+ if (!roleId) {
+ return null;
+ }
+ return trpcUtils.authorization.roles.connectedPerms.getData({
+ roleId,
+ });
+ };
+
+ const calculateLimits = (
+ additionalKeys?: string[],
+ additionalPerms?: string[],
+ ): RoleLimitState => {
+ const keysPreview = getKeysPreview();
+ const permsPreview = getPermsPreview();
+
+ // Calculate totals - use preview data first, fallback to additional arrays
+ const totalKeys = keysPreview?.totalCount || additionalKeys?.length || 0;
+
+ const totalPerms = permsPreview?.totalCount || additionalPerms?.length || 0;
+
+ // Only show warnings for existing roles (edit mode)
+ const hasKeyWarning = Boolean(roleId && totalKeys > MAX_ATTACH_LIMIT);
+ const hasPermWarning = Boolean(roleId && totalPerms > MAX_ATTACH_LIMIT);
+
+ // Should prefetch when both are under limit
+ const shouldPrefetch = totalKeys <= MAX_ATTACH_LIMIT && totalPerms <= MAX_ATTACH_LIMIT;
+
+ // Should allow editing when both are under limit (or it's create mode)
+ const shouldAllowEdit = !roleId || shouldPrefetch;
+
+ return {
+ totalKeys,
+ totalPerms,
+ hasKeyWarning,
+ hasPermWarning,
+ shouldPrefetch,
+ shouldAllowEdit,
+ };
+ };
+
+ const prefetchIfAllowed = async () => {
+ if (!roleId) {
+ return;
+ }
+
+ const { shouldPrefetch } = calculateLimits();
+
+ if (shouldPrefetch) {
+ await trpcUtils.authorization.roles.connectedKeysAndPerms.prefetch({
+ roleId,
+ });
+ }
+ };
+
+ return {
+ calculateLimits,
+ prefetchIfAllowed,
+ MAX_ATTACH_LIMIT,
+ };
+};
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts b/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts
index 7d89d995c9..c0ae0e8e8b 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts
+++ b/apps/dashboard/app/(app)/authorization/roles/components/table/hooks/use-roles-list-query.ts
@@ -1,5 +1,5 @@
import { trpc } from "@/lib/trpc/client";
-import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
+import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { useEffect, useMemo, useState } from "react";
import { rolesFilterFieldConfig, rolesListFilterFieldNames } from "../../../filters.schema";
import { useFilters } from "../../../hooks/use-filters";
@@ -7,7 +7,7 @@ import type { RolesQueryPayload } from "../query-logs.schema";
export function useRolesListQuery() {
const [totalCount, setTotalCount] = useState(0);
- const [rolesMap, setRolesMap] = useState(() => new Map
());
+ const [rolesMap, setRolesMap] = useState(() => new Map());
const { filters } = useFilters();
const rolesList = useMemo(() => Array.from(rolesMap.values()), [rolesMap]);
@@ -54,7 +54,7 @@ export function useRolesListQuery() {
useEffect(() => {
if (rolesData) {
- const newMap = new Map();
+ const newMap = new Map();
rolesData.pages.forEach((page) => {
page.roles.forEach((role) => {
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx b/apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
index 730acbbab0..fb4056b34c 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/components/table/roles-list.tsx
@@ -1,7 +1,7 @@
"use client";
import { VirtualTable } from "@/components/virtual-table/index";
import type { Column } from "@/components/virtual-table/types";
-import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
+import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { BookBookmark, Tag } from "@unkey/icons";
import { Button, Checkbox, Empty } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
@@ -23,7 +23,7 @@ import { getRowClassName } from "./utils/get-row-class";
export const RolesList = () => {
const { roles, isLoading, isLoadingMore, loadMore, totalCount, hasMore } = useRolesListQuery();
- const [selectedRole, setSelectedRole] = useState(null);
+ const [selectedRole, setSelectedRole] = useState(null);
const [selectedRoles, setSelectedRoles] = useState>(new Set());
const [hoveredRoleName, setHoveredRoleName] = useState(null);
@@ -39,7 +39,7 @@ export const RolesList = () => {
});
}, []);
- const columns: Column[] = useMemo(
+ const columns: Column[] = useMemo(
() => [
{
key: "role",
@@ -103,23 +103,21 @@ export const RolesList = () => {
width: "20%",
render: (role) => (
),
},
{
key: "permissions",
- header: "Permissions",
+ header: "Assigned Permissions",
width: "20%",
render: (role) => (
),
},
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/table/utils/get-row-class.ts b/apps/dashboard/app/(app)/authorization/roles/components/table/utils/get-row-class.ts
index ffe7759263..a2d6856b60 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/table/utils/get-row-class.ts
+++ b/apps/dashboard/app/(app)/authorization/roles/components/table/utils/get-row-class.ts
@@ -1,4 +1,4 @@
-import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
+import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { cn } from "@/lib/utils";
export type StatusStyle = {
@@ -23,9 +23,9 @@ export const STATUS_STYLES = {
focusRing: "focus:ring-accent-7",
};
-export const getRowClassName = (log: Roles, selectedLog: Roles | null) => {
+export const getRowClassName = (role: RoleBasic, selectedLog: RoleBasic | null) => {
const style = STATUS_STYLES;
- const isSelected = log.roleId === selectedLog?.roleId;
+ const isSelected = role.roleId === selectedLog?.roleId;
return cn(
style.base,
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-key/key-field.tsx b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-key/key-field.tsx
index 5fe65eb1da..0734900b06 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-key/key-field.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-key/key-field.tsx
@@ -3,6 +3,8 @@ import { FormCombobox } from "@/components/ui/form-combobox";
import type { RoleKey } from "@/lib/trpc/routers/authorization/roles/connected-keys-and-perms";
import { Key2 } from "@unkey/icons";
import { useMemo, useState } from "react";
+import { useRoleLimits } from "../../../table/hooks/use-role-limits";
+import { RoleWarningCallout } from "../warning-callout";
import { createKeyOptions } from "./create-key-options";
import { useFetchKeys } from "./hooks/use-fetch-keys";
import { useSearchKeys } from "./hooks/use-search-keys";
@@ -25,28 +27,29 @@ export const KeyField = ({
assignedKeyDetails,
}: KeyFieldProps) => {
const [searchValue, setSearchValue] = useState("");
+
+ const { calculateLimits } = useRoleLimits(roleId);
+ const { hasKeyWarning, totalKeys } = calculateLimits(value);
+
const { keys, isFetchingNextPage, hasNextPage, loadMore, isLoading } = useFetchKeys();
const { searchResults, isSearching } = useSearchKeys(searchValue);
- // Combine loaded keys with search results, prioritizing search when available
const allKeys = useMemo(() => {
if (searchValue.trim() && searchResults.length > 0) {
- // When searching, use search results
return searchResults;
}
+
if (searchValue.trim() && searchResults.length === 0 && !isSearching) {
- // No search results found, filter from loaded keys as fallback
const searchTerm = searchValue.toLowerCase().trim();
return keys.filter(
(key) =>
key.id.toLowerCase().includes(searchTerm) || key.name?.toLowerCase().includes(searchTerm),
);
}
- // No search query, use all loaded keys
+
return keys;
}, [keys, searchResults, searchValue, isSearching]);
- // Don't show load more when actively searching
const showLoadMore = !searchValue.trim() && hasNextPage;
const baseOptions = createKeyOptions({
@@ -59,23 +62,19 @@ export const KeyField = ({
const selectableOptions = useMemo(() => {
return baseOptions.filter((option) => {
- // Always allow the load more option
if (option.value === "__load_more__") {
return true;
}
- // Don't show already selected keys
if (value.includes(option.value)) {
return false;
}
- // Find the key and check if it's already assigned to this role
const key = allKeys.find((k) => k.id === option.value);
if (!key) {
return true;
}
- // Filter out keys that already have this role assigned (if roleId provided)
if (roleId) {
return !key.roles.some((role) => role.id === roleId);
}
@@ -87,7 +86,6 @@ export const KeyField = ({
const selectedKeys = useMemo(() => {
return value
.map((keyId) => {
- // check selectedKeysData (for pre-loaded edit data)
const preLoadedKey = assignedKeyDetails.find((k) => k.id === keyId);
if (preLoadedKey) {
return {
@@ -96,7 +94,6 @@ export const KeyField = ({
};
}
- // check loaded keys (for newly added keys)
const loadedKey = allKeys.find((k) => k.id === keyId);
if (loadedKey) {
return {
@@ -105,7 +102,6 @@ export const KeyField = ({
};
}
- // Third: fallback to ID-only display (ensures key is always removable)
return {
id: keyId,
name: null,
@@ -160,7 +156,7 @@ export const KeyField = ({
}
variant="default"
error={error}
- disabled={disabled || isLoading}
+ disabled={disabled || isLoading || hasKeyWarning}
loading={isComboboxLoading}
title={
isComboboxLoading
@@ -170,22 +166,25 @@ export const KeyField = ({
: undefined
}
/>
-
- ({
- ...k,
- name: k.name ?? "Unnamed Key",
- }))}
- disabled={disabled}
- onRemoveItem={handleRemoveKey}
- renderIcon={() => }
- enableTransitions
- renderPrimaryText={(key) =>
- key.id.length > 15 ? `${key.id.slice(0, 8)}...${key.id.slice(-4)}` : key.id
- }
- renderSecondaryText={(key) => key.name || "Unnamed Key"}
- itemHeight="h-12"
- />
+ {hasKeyWarning ? (
+
+ ) : (
+ ({
+ ...k,
+ name: k.name ?? "Unnamed Key",
+ }))}
+ disabled={disabled}
+ onRemoveItem={handleRemoveKey}
+ renderIcon={() => }
+ enableTransitions
+ renderPrimaryText={(key) =>
+ key.id.length > 15 ? `${key.id.slice(0, 8)}...${key.id.slice(-4)}` : key.id
+ }
+ renderSecondaryText={(key) => key.name || "Unnamed Key"}
+ itemHeight="h-12"
+ />
+ )}
);
};
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-permission/permissions-field.tsx b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-permission/permissions-field.tsx
index b1802b9427..f1b9a19439 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-permission/permissions-field.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/assign-permission/permissions-field.tsx
@@ -3,6 +3,8 @@ import { FormCombobox } from "@/components/ui/form-combobox";
import type { RolePermission } from "@/lib/trpc/routers/authorization/roles/connected-keys-and-perms";
import { Page2 } from "@unkey/icons";
import { useMemo, useState } from "react";
+import { useRoleLimits } from "../../../table/hooks/use-role-limits";
+import { RoleWarningCallout } from "../warning-callout";
import { createPermissionOptions } from "./create-permission-options";
import { useFetchPermissions } from "./hooks/use-fetch-permissions";
import { useSearchPermissions } from "./hooks/use-search-permissions";
@@ -15,16 +17,19 @@ type PermissionFieldProps = {
roleId?: string;
assignedPermsDetails: RolePermission[];
};
-
export const PermissionField = ({
value,
onChange,
error,
disabled = false,
roleId,
- assignedPermsDetails = [],
+ assignedPermsDetails,
}: PermissionFieldProps) => {
const [searchValue, setSearchValue] = useState("");
+
+ const { calculateLimits } = useRoleLimits(roleId);
+ const { hasPermWarning, totalPerms } = calculateLimits(value);
+
const { permissions, isFetchingNextPage, hasNextPage, loadMore, isLoading } =
useFetchPermissions();
const { searchResults, isSearching } = useSearchPermissions(searchValue);
@@ -32,7 +37,6 @@ export const PermissionField = ({
// Combine loaded permissions with search results, prioritizing search when available
const allPermissions = useMemo(() => {
if (searchValue.trim() && searchResults.length > 0) {
- // When searching, use search results
return searchResults;
}
if (searchValue.trim() && searchResults.length === 0 && !isSearching) {
@@ -63,12 +67,9 @@ export const PermissionField = ({
const selectableOptions = useMemo(() => {
return baseOptions.filter((option) => {
- // Always allow the load more option
if (option.value === "__load_more__") {
return true;
}
-
- // Don't show already selected permissions
if (value.includes(option.value)) {
return false;
}
@@ -81,34 +82,36 @@ export const PermissionField = ({
// Filter out permissions that already have this role assigned (if roleId provided)
if (roleId) {
- return !permission.roles.some((role) => role.id === roleId);
+ return !permission.roles?.some((role) => role.id === roleId);
}
-
return true;
});
}, [baseOptions, allPermissions, roleId, value]);
const selectedPermissions = useMemo(() => {
return value
- .map((id) => {
+ .map((permId) => {
// First: check selectedPermissionsData (for pre-loaded edit data)
- const preLoadedPerm = assignedPermsDetails.find((p) => p.id === id);
+ const preLoadedPerm = assignedPermsDetails.find((p) => p.id === permId);
if (preLoadedPerm) {
- return preLoadedPerm;
+ return {
+ id: preLoadedPerm.id,
+ name: preLoadedPerm.name,
+ slug: preLoadedPerm.slug,
+ };
}
// Second: check loaded permissions (for newly added permissions)
- const loadedPerm = allPermissions.find((p) => p.id === id);
+ const loadedPerm = allPermissions.find((p) => p.id === permId);
if (loadedPerm) {
return loadedPerm;
}
// Third: fallback
return {
- id: id,
- name: id,
- slug: id,
- description: null,
+ id: permId,
+ name: null,
+ slug: null,
};
})
.filter((perm): perm is NonNullable
=> perm !== undefined);
@@ -160,7 +163,7 @@ export const PermissionField = ({
}
variant="default"
error={error}
- disabled={disabled || isLoading}
+ disabled={disabled || isLoading || hasPermWarning}
loading={isComboboxLoading}
title={
isComboboxLoading
@@ -170,17 +173,25 @@ export const PermissionField = ({
: undefined
}
/>
-
- {/* Selected Permissions Display */}
- }
- renderPrimaryText={(permission) => permission.name}
- enableTransitions
- renderSecondaryText={(permission) => permission.slug}
- />
+ {hasPermWarning ? (
+
+ ) : (
+ selectedPermissions.length > 0 && (
+ ({
+ ...k,
+ name: k.name ?? "Unnamed Permission",
+ }))}
+ disabled={disabled}
+ onRemoveItem={handleRemovePermission}
+ renderIcon={() => }
+ renderPrimaryText={(permission) => permission.name}
+ enableTransitions
+ // This can't cannot happen but we need it to make TS happy
+ renderSecondaryText={(permission) => permission.slug ?? "Unnamed Slug"}
+ />
+ )
+ )}
);
};
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/warning-callout.tsx b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/warning-callout.tsx
new file mode 100644
index 0000000000..49534e7e2a
--- /dev/null
+++ b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/components/warning-callout.tsx
@@ -0,0 +1,32 @@
+import { formatNumber } from "@/lib/fmt";
+import { TriangleWarning } from "@unkey/icons";
+import { InlineLink } from "@unkey/ui";
+
+interface RoleWarningCalloutProps {
+ count: number;
+ type: "keys" | "permissions";
+}
+
+export const RoleWarningCallout = ({ count, type }: RoleWarningCalloutProps) => {
+ const itemText = type === "keys" ? "keys" : "permissions";
+ const settingsText = type === "keys" ? "key settings" : "permission settings";
+
+ return (
+
+
+
+
+
+ Warning: This role has {formatNumber(count)} {itemText}{" "}
+ assigned. Use the{" "}
+ {" "}
+ or {settingsText} to manage these assignments.
+
+
+ );
+};
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/hooks/use-upsert-role.ts b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/hooks/use-upsert-role.ts
index 7ffd914d3b..dc763af2da 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/hooks/use-upsert-role.ts
+++ b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/hooks/use-upsert-role.ts
@@ -11,8 +11,20 @@ export const useUpsertRole = (
const trpcUtils = trpc.useUtils();
const role = trpc.authorization.roles.upsert.useMutation({
- onSuccess(data) {
- trpcUtils.authorization.roles.invalidate();
+ async onSuccess(data) {
+ await Promise.all([
+ trpcUtils.authorization.roles.query.invalidate(),
+ trpcUtils.authorization.permissions.query.refetch(),
+ trpcUtils.authorization.roles.connectedKeysAndPerms.invalidate({
+ roleId: data.roleId,
+ }),
+ trpcUtils.authorization.roles.connectedKeys.invalidate({
+ roleId: data.roleId,
+ }),
+ trpcUtils.authorization.roles.connectedPerms.invalidate({
+ roleId: data.roleId,
+ }),
+ ]);
// Show success toast
toast.success(data.isUpdate ? "Role Updated" : "Role Created", {
diff --git a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/index.tsx b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/index.tsx
index 7a542f01f9..29a2451370 100644
--- a/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/index.tsx
+++ b/apps/dashboard/app/(app)/authorization/roles/components/upsert-role/index.tsx
@@ -11,6 +11,7 @@ import { PenWriting3, Plus } from "@unkey/icons";
import { Button, DialogContainer, FormInput, FormTextarea } from "@unkey/ui";
import { useEffect, useState } from "react";
import { Controller, FormProvider } from "react-hook-form";
+import { useRoleLimits } from "../table/hooks/use-role-limits";
import { KeyField } from "./components/assign-key/key-field";
import { PermissionField } from "./components/assign-permission/permissions-field";
import { useUpsertRole } from "./hooks/use-upsert-role";
@@ -35,7 +36,7 @@ const getDefaultValues = (existingRole?: ExistingRole): Partial
=> {
roleName: existingRole.name,
roleDescription: existingRole.description || "",
keyIds: existingRole.keyIds || [],
- permissionIds: existingRole.permissionIds,
+ permissionIds: existingRole.permissionIds || [],
};
}
@@ -63,6 +64,8 @@ export const UpsertRoleDialog = ({
const [internalIsOpen, setInternalIsOpen] = useState(false);
const isEditMode = Boolean(existingRole?.id);
+ const { calculateLimits } = useRoleLimits(existingRole?.id);
+
// Use external state if provided, otherwise use internal state
const isDialogOpen = externalIsOpen !== undefined ? externalIsOpen : internalIsOpen;
const setIsDialogOpen =
@@ -126,7 +129,16 @@ export const UpsertRoleDialog = ({
});
const onSubmit = async (data: FormValues) => {
- upsertRoleMutation.mutate(data);
+ // Calculate limits with current form data
+ const { hasKeyWarning, hasPermWarning } = calculateLimits(data.keyIds, data.permissionIds);
+
+ const submissionData: FormValues = {
+ ...data,
+ keyIds: hasKeyWarning ? undefined : data.keyIds,
+ permissionIds: hasPermWarning ? undefined : data.permissionIds,
+ };
+
+ upsertRoleMutation.mutate(submissionData);
};
const handleDialogToggle = (open: boolean) => {
@@ -213,6 +225,7 @@ export const UpsertRoleDialog = ({
control={control}
render={({ field, fieldState }) => (
(
[...new Set(ids)])
- .optional();
+export const keyIdsSchema = z.array(z.string()).transform((ids) => [...new Set(ids)]);
-export const permissionIdsSchema = z
- .array(z.string())
- .default([])
- .transform((ids) => [...new Set(ids)])
- .optional();
+export const permissionIdsSchema = z.array(z.string()).transform((ids) => [...new Set(ids)]);
export const rbacRoleSchema = z
.object({
roleId: z.string().startsWith("role_").optional(),
roleName: roleNameSchema,
roleDescription: roleDescriptionSchema,
- keyIds: keyIdsSchema,
- permissionIds: permissionIdsSchema,
+ keyIds: keyIdsSchema.optional(),
+ permissionIds: permissionIdsSchema.optional(),
})
.strict({ message: "Unknown fields are not allowed in role definition" });
diff --git a/apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts b/apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts
index 281712138b..4d56eb0bde 100644
--- a/apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts
+++ b/apps/dashboard/lib/trpc/routers/authorization/permissions/query.ts
@@ -4,8 +4,6 @@ import { db, sql } from "@/lib/db";
import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
import { z } from "zod";
-const MAX_ITEMS_TO_SHOW = 3;
-const ITEM_SEPARATOR = "|||";
export const DEFAULT_LIMIT = 50;
export const permissions = z.object({
@@ -15,10 +13,7 @@ export const permissions = z.object({
slug: z.string(),
lastUpdated: z.number(),
totalConnectedKeys: z.number(),
- assignedRoles: z.object({
- items: z.array(z.string()),
- totalCount: z.number().optional(),
- }),
+ totalConnectedRoles: z.number(),
});
export type Permission = z.infer;
@@ -57,21 +52,6 @@ export const queryPermissions = t.procedure
p.slug,
p.updated_at_m,
- -- Roles: get first 3 unique names
- (
- SELECT GROUP_CONCAT(sub.name ORDER BY sub.name SEPARATOR ${ITEM_SEPARATOR})
- FROM (
- SELECT DISTINCT r.name
- FROM roles_permissions rp
- LEFT JOIN roles r ON rp.role_id = r.id
- WHERE rp.permission_id = p.id
- AND rp.workspace_id = ${workspaceId}
- AND r.name IS NOT NULL
- ORDER BY r.name
- LIMIT ${MAX_ITEMS_TO_SHOW}
- ) sub
- ) as role_items,
-
-- Roles: total count
(
SELECT COUNT(DISTINCT rp.role_id)
@@ -121,7 +101,6 @@ export const queryPermissions = t.procedure
description: string | null;
slug: string;
updated_at_m: number;
- role_items: string | null;
total_roles: number;
total_connected_keys: number;
grand_total: number;
@@ -141,21 +120,13 @@ export const queryPermissions = t.procedure
const items = hasMore ? rows.slice(0, -1) : rows;
const permissionsResponseData: Permission[] = items.map((row) => {
- // Parse concatenated strings back to arrays
- const roleItems = row.role_items
- ? row.role_items.split(ITEM_SEPARATOR).filter((item) => item.trim() !== "")
- : [];
-
return {
permissionId: row.id,
name: row.name || "",
description: row.description || "",
slug: row.slug || "",
lastUpdated: Number(row.updated_at_m) || 0,
- assignedRoles:
- row.total_roles <= MAX_ITEMS_TO_SHOW
- ? { items: roleItems }
- : { items: roleItems, totalCount: Number(row.total_roles) },
+ totalConnectedRoles: Number(row.total_roles),
totalConnectedKeys: Number(row.total_connected_keys) || 0,
};
});
diff --git a/apps/dashboard/lib/trpc/routers/authorization/roles/keys/connected-keys.ts b/apps/dashboard/lib/trpc/routers/authorization/roles/keys/connected-keys.ts
new file mode 100644
index 0000000000..f4e07f959c
--- /dev/null
+++ b/apps/dashboard/lib/trpc/routers/authorization/roles/keys/connected-keys.ts
@@ -0,0 +1,30 @@
+import { and, count, db, eq } from "@/lib/db";
+import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
+import { keysRoles } from "@unkey/db/src/schema";
+import { z } from "zod";
+
+const assignedKeysResponse = z.object({
+ totalCount: z.number(),
+});
+
+export const queryRoleKeys = t.procedure
+ .use(requireUser)
+ .use(requireWorkspace)
+ .use(withRatelimit(ratelimit.read))
+ .input(
+ z.object({
+ roleId: z.string(),
+ }),
+ )
+ .output(assignedKeysResponse)
+ .query(async ({ ctx, input }) => {
+ const workspaceId = ctx.workspace.id;
+ const { roleId } = input;
+
+ const result = await db
+ .select({ count: count() })
+ .from(keysRoles)
+ .where(and(eq(keysRoles.workspaceId, workspaceId), eq(keysRoles.roleId, roleId)));
+
+ return { totalCount: result[0].count };
+ });
diff --git a/apps/dashboard/lib/trpc/routers/authorization/roles/permissions/connected-permissions.ts b/apps/dashboard/lib/trpc/routers/authorization/roles/permissions/connected-permissions.ts
new file mode 100644
index 0000000000..ed7514b1fd
--- /dev/null
+++ b/apps/dashboard/lib/trpc/routers/authorization/roles/permissions/connected-permissions.ts
@@ -0,0 +1,32 @@
+import { and, count, db, eq } from "@/lib/db";
+import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
+import { rolesPermissions } from "@unkey/db/src/schema";
+import { z } from "zod";
+
+const permissionsResponse = z.object({
+ totalCount: z.number(),
+});
+
+export const queryRolePermissions = t.procedure
+ .use(requireUser)
+ .use(requireWorkspace)
+ .use(withRatelimit(ratelimit.read))
+ .input(
+ z.object({
+ roleId: z.string(),
+ }),
+ )
+ .output(permissionsResponse)
+ .query(async ({ ctx, input }) => {
+ const workspaceId = ctx.workspace.id;
+ const { roleId } = input;
+
+ const result = await db
+ .select({ count: count() })
+ .from(rolesPermissions)
+ .where(
+ and(eq(rolesPermissions.workspaceId, workspaceId), eq(rolesPermissions.roleId, roleId)),
+ );
+
+ return { totalCount: result?.[0]?.count ?? 0 };
+ });
diff --git a/apps/dashboard/lib/trpc/routers/authorization/roles/query.ts b/apps/dashboard/lib/trpc/routers/authorization/roles/query.ts
index bc02c2a87b..40efadf624 100644
--- a/apps/dashboard/lib/trpc/routers/authorization/roles/query.ts
+++ b/apps/dashboard/lib/trpc/routers/authorization/roles/query.ts
@@ -4,29 +4,19 @@ import { db, sql } from "@/lib/db";
import { ratelimit, requireUser, requireWorkspace, t, withRatelimit } from "@/lib/trpc/trpc";
import { z } from "zod";
-const MAX_ITEMS_TO_SHOW = 3;
-const ITEM_SEPARATOR = "|||";
export const DEFAULT_LIMIT = 50;
-export const roles = z.object({
+export const roleBasic = z.object({
roleId: z.string(),
name: z.string(),
description: z.string(),
lastUpdated: z.number(),
- assignedKeys: z.object({
- items: z.array(z.string()),
- totalCount: z.number().optional(),
- }),
- permissions: z.object({
- items: z.array(z.string()),
- totalCount: z.number().optional(),
- }),
});
-export type Roles = z.infer;
+export type RoleBasic = z.infer;
const rolesResponse = z.object({
- roles: z.array(roles),
+ roles: z.array(roleBasic),
hasMore: z.boolean(),
total: z.number(),
nextCursor: z.number().int().nullish(),
@@ -48,114 +38,20 @@ export const queryRoles = t.procedure
const keyFilter = buildKeyFilter(keyName, keyId, workspaceId);
const permissionFilter = buildPermissionFilter(permissionName, permissionSlug, workspaceId);
- // Build filter conditions for total count
- const keyFilterForCount = buildKeyFilter(keyName, keyId, workspaceId);
- const permissionFilterForCount = buildPermissionFilter(
- permissionName,
- permissionSlug,
- workspaceId,
- );
+ // Get total count first
+ const countResult = await db.execute(sql`
+ SELECT COUNT(*) as total
+ FROM roles
+ WHERE workspace_id = ${workspaceId}
+ ${nameFilter}
+ ${descriptionFilter}
+ ${keyFilter}
+ ${permissionFilter}
+ `);
- const result = await db.execute(sql`
- SELECT
- r.id,
- r.name,
- r.description,
- r.updated_at_m,
-
- -- Keys: get first 3 unique names
- (
- SELECT GROUP_CONCAT(sub.display_name ORDER BY sub.sort_key SEPARATOR ${ITEM_SEPARATOR})
- FROM (
- SELECT DISTINCT
- CASE
- WHEN k.name IS NULL OR k.name = '' THEN
- CONCAT(SUBSTRING(k.id, 1, 8), '...', RIGHT(k.id, 4))
- ELSE k.name
- END as display_name,
- COALESCE(k.name, k.id) as sort_key
- FROM keys_roles kr
- JOIN \`keys\` k ON kr.key_id = k.id
- WHERE kr.role_id = r.id
- AND kr.workspace_id = ${workspaceId}
- ORDER BY sort_key
- LIMIT ${MAX_ITEMS_TO_SHOW}
- ) sub
- ) as key_items,
-
- -- Keys: total count
- (
- SELECT COUNT(DISTINCT kr.key_id)
- FROM keys_roles kr
- JOIN \`keys\` k ON kr.key_id = k.id
- WHERE kr.role_id = r.id
- AND kr.workspace_id = ${workspaceId}
- ) as total_keys,
-
- -- Permissions: get first 3 unique names
- (
- SELECT GROUP_CONCAT(sub.name ORDER BY sub.name SEPARATOR ${ITEM_SEPARATOR})
- FROM (
- SELECT DISTINCT p.name
- FROM roles_permissions rp
- JOIN permissions p ON rp.permission_id = p.id
- WHERE rp.role_id = r.id
- AND rp.workspace_id = ${workspaceId}
- AND p.name IS NOT NULL
- ORDER BY p.name
- LIMIT ${MAX_ITEMS_TO_SHOW}
- ) sub
- ) as permission_items,
-
- -- Permissions: total count
- (
- SELECT COUNT(DISTINCT rp.permission_id)
- FROM roles_permissions rp
- JOIN permissions p ON rp.permission_id = p.id
- WHERE rp.role_id = r.id
- AND rp.workspace_id = ${workspaceId}
- AND p.name IS NOT NULL
- ) as total_permissions,
-
- -- Total count of roles (with filters applied)
- (
- SELECT COUNT(*)
- FROM roles
- WHERE workspace_id = ${workspaceId}
- ${nameFilter}
- ${descriptionFilter}
- ${keyFilterForCount}
- ${permissionFilterForCount}
- ) as grand_total
-
- FROM (
- SELECT id, name, description, updated_at_m
- FROM roles
- WHERE workspace_id = ${workspaceId}
- ${cursor ? sql`AND updated_at_m < ${cursor}` : sql``}
- ${nameFilter}
- ${descriptionFilter}
- ${keyFilter}
- ${permissionFilter}
- ORDER BY updated_at_m DESC
- LIMIT ${DEFAULT_LIMIT + 1}
- ) r
- ORDER BY r.updated_at_m DESC
-`);
+ const total = (countResult.rows[0] as { total: number }).total;
- const rows = result.rows as {
- id: string;
- name: string;
- description: string | null;
- updated_at_m: number;
- key_items: string | null;
- total_keys: number;
- permission_items: string | null;
- total_permissions: number;
- grand_total: number;
- }[];
-
- if (rows.length === 0) {
+ if (total === 0) {
return {
roles: [],
hasMore: false,
@@ -164,37 +60,35 @@ export const queryRoles = t.procedure
};
}
- const total = rows[0].grand_total;
+ const result = await db.execute(sql`
+ SELECT id, name, description, updated_at_m
+ FROM roles
+ WHERE workspace_id = ${workspaceId}
+ ${cursor ? sql`AND updated_at_m < ${cursor}` : sql``}
+ ${nameFilter}
+ ${descriptionFilter}
+ ${keyFilter}
+ ${permissionFilter}
+ ORDER BY updated_at_m DESC
+ LIMIT ${DEFAULT_LIMIT + 1}
+ `);
+
+ const rows = result.rows as {
+ id: string;
+ name: string;
+ description: string | null;
+ updated_at_m: number;
+ }[];
+
const hasMore = rows.length > DEFAULT_LIMIT;
const items = hasMore ? rows.slice(0, -1) : rows;
- const rolesResponseData: Roles[] = items.map((row) => {
- // Parse concatenated strings back to arrays
- const keyItems = row.key_items
- ? row.key_items.split(ITEM_SEPARATOR).filter((item) => item.trim() !== "")
- : [];
- const permissionItems = row.permission_items
- ? row.permission_items.split(ITEM_SEPARATOR).filter((item) => item.trim() !== "")
- : [];
-
- return {
- roleId: row.id,
- name: row.name || "",
- description: row.description || "",
- lastUpdated: Number(row.updated_at_m) || 0,
- assignedKeys:
- row.total_keys <= MAX_ITEMS_TO_SHOW
- ? { items: keyItems }
- : { items: keyItems, totalCount: Number(row.total_keys) },
- permissions:
- row.total_permissions <= MAX_ITEMS_TO_SHOW
- ? { items: permissionItems }
- : {
- items: permissionItems,
- totalCount: Number(row.total_permissions),
- },
- };
- });
+ const rolesResponseData: RoleBasic[] = items.map((row) => ({
+ roleId: row.id,
+ name: row.name || "",
+ description: row.description || "",
+ lastUpdated: Number(row.updated_at_m) || 0,
+ }));
return {
roles: rolesResponseData,
@@ -226,7 +120,6 @@ function buildKeyFilter(
) {
const conditions = [];
- // Handle name filters
if (nameFilters && nameFilters.length > 0) {
const nameConditions = nameFilters.map((filter) => {
const value = filter.value;
@@ -270,7 +163,6 @@ function buildKeyFilter(
conditions.push(sql`(${sql.join(nameConditions, sql` OR `)})`);
}
- // Handle ID filters
if (idFilters && idFilters.length > 0) {
const idConditions = idFilters.map((filter) => {
const value = filter.value;
@@ -318,7 +210,6 @@ function buildKeyFilter(
return sql``;
}
- // Join name and ID conditions with AND
return sql`AND (${sql.join(conditions, sql` AND `)})`;
}
@@ -352,7 +243,6 @@ function buildFilterConditions(
}
});
- // Combine conditions with OR
return sql`AND (${sql.join(conditions, sql` OR `)})`;
}
@@ -375,7 +265,6 @@ function buildPermissionFilter(
) {
const conditions = [];
- // Handle name filters
if (nameFilters && nameFilters.length > 0) {
const nameConditions = nameFilters.map((filter) => {
const value = filter.value;
@@ -419,7 +308,6 @@ function buildPermissionFilter(
conditions.push(sql`(${sql.join(nameConditions, sql` OR `)})`);
}
- // Handle slug filters
if (slugFilters && slugFilters.length > 0) {
const slugConditions = slugFilters.map((filter) => {
const value = filter.value;
@@ -467,6 +355,5 @@ function buildPermissionFilter(
return sql``;
}
- // Join name and slug conditions with AND
return sql`AND (${sql.join(conditions, sql` AND `)})`;
}
diff --git a/apps/dashboard/lib/trpc/routers/authorization/roles/upsert.ts b/apps/dashboard/lib/trpc/routers/authorization/roles/upsert.ts
index 47a898c783..0f68948c01 100644
--- a/apps/dashboard/lib/trpc/routers/authorization/roles/upsert.ts
+++ b/apps/dashboard/lib/trpc/routers/authorization/roles/upsert.ts
@@ -33,6 +33,7 @@ export const upsertRole = t.procedure
await db.transaction(async (tx) => {
if (isUpdate && input.roleId) {
const updateRoleId: string = input.roleId;
+
// Get the existing role to compare names and verify existence
const existingRole = await tx.query.roles.findFirst({
where: (table, { and, eq }) =>
@@ -80,25 +81,111 @@ export const upsertRole = t.procedure
});
});
- // Remove existing role-permission relationships
- await tx
- .delete(schema.rolesPermissions)
- .where(
- and(
- eq(schema.rolesPermissions.roleId, roleId),
- eq(schema.rolesPermissions.workspaceId, ctx.workspace.id),
- ),
- );
+ // Handle permissions - only modify if explicitly provided
+ if (input.permissionIds !== undefined) {
+ // Remove existing role-permission relationships
+ await tx
+ .delete(schema.rolesPermissions)
+ .where(
+ and(
+ eq(schema.rolesPermissions.roleId, roleId),
+ eq(schema.rolesPermissions.workspaceId, ctx.workspace.id),
+ ),
+ );
- // Remove existing key-role relationships
- await tx
- .delete(schema.keysRoles)
- .where(
- and(
- eq(schema.keysRoles.roleId, roleId),
- eq(schema.keysRoles.workspaceId, ctx.workspace.id),
- ),
- );
+ // Add new permissions if any
+ if (input.permissionIds.length > 0) {
+ await tx
+ .insert(schema.rolesPermissions)
+ .values(
+ input.permissionIds.map((permissionId) => ({
+ permissionId,
+ roleId,
+ workspaceId: ctx.workspace.id,
+ })),
+ )
+ .catch(() => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Failed to assign permissions to role",
+ });
+ });
+
+ await insertAuditLogs(
+ tx,
+ input.permissionIds.map((permissionId) => ({
+ workspaceId: ctx.workspace.id,
+ event: "authorization.connect_role_and_permission",
+ actor: {
+ type: "user",
+ id: ctx.user.id,
+ },
+ description: `Connected role ${roleId} and permission ${permissionId}`,
+ resources: [
+ { type: "role", id: roleId, name: input.roleName },
+ { type: "permission", id: permissionId },
+ ],
+ context: {
+ userAgent: ctx.audit.userAgent,
+ location: ctx.audit.location,
+ },
+ })),
+ );
+ }
+ }
+
+ // Handle keys - only modify if explicitly provided
+ if (input.keyIds !== undefined) {
+ // Remove existing key-role relationships
+ await tx
+ .delete(schema.keysRoles)
+ .where(
+ and(
+ eq(schema.keysRoles.roleId, roleId),
+ eq(schema.keysRoles.workspaceId, ctx.workspace.id),
+ ),
+ );
+
+ // Add new keys if any
+ if (input.keyIds.length > 0) {
+ await tx
+ .insert(schema.keysRoles)
+ .values(
+ input.keyIds.map((keyId) => ({
+ keyId,
+ roleId,
+ workspaceId: ctx.workspace.id,
+ })),
+ )
+ .catch(() => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Failed to assign keys to role",
+ });
+ });
+
+ await insertAuditLogs(
+ tx,
+ input.keyIds.map((keyId) => ({
+ workspaceId: ctx.workspace.id,
+ event: "authorization.connect_role_and_key",
+ actor: {
+ type: "user",
+ id: ctx.user.id,
+ },
+ description: `Connected key ${keyId} and role ${roleId}`,
+ resources: [
+ { type: "key", id: keyId },
+ { type: "role", id: roleId, name: input.roleName },
+ ],
+ context: {
+ userAgent: ctx.audit.userAgent,
+ location: ctx.audit.location,
+ },
+ })),
+ );
+ }
+ }
await insertAuditLogs(tx, {
workspaceId: ctx.workspace.id,
@@ -139,7 +226,7 @@ export const upsertRole = t.procedure
.insert(schema.roles)
.values({
id: roleId,
- name: input.roleName, // name maps to db.human_readable
+ name: input.roleName,
description: input.roleDescription,
workspaceId: ctx.workspace.id,
})
@@ -150,6 +237,90 @@ export const upsertRole = t.procedure
});
});
+ // For creation, treat undefined as empty array (no associations initially)
+ const permissionIds = input.permissionIds ?? [];
+ const keyIds = input.keyIds ?? [];
+
+ // Add role-permission relationships
+ if (permissionIds.length > 0) {
+ await tx
+ .insert(schema.rolesPermissions)
+ .values(
+ permissionIds.map((permissionId) => ({
+ permissionId,
+ roleId,
+ workspaceId: ctx.workspace.id,
+ })),
+ )
+ .catch(() => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Failed to assign permissions to role",
+ });
+ });
+
+ await insertAuditLogs(
+ tx,
+ permissionIds.map((permissionId) => ({
+ workspaceId: ctx.workspace.id,
+ event: "authorization.connect_role_and_permission",
+ actor: {
+ type: "user",
+ id: ctx.user.id,
+ },
+ description: `Connected role ${roleId} and permission ${permissionId}`,
+ resources: [
+ { type: "role", id: roleId, name: input.roleName },
+ { type: "permission", id: permissionId },
+ ],
+ context: {
+ userAgent: ctx.audit.userAgent,
+ location: ctx.audit.location,
+ },
+ })),
+ );
+ }
+
+ // Add key-role relationships
+ if (keyIds.length > 0) {
+ await tx
+ .insert(schema.keysRoles)
+ .values(
+ keyIds.map((keyId) => ({
+ keyId,
+ roleId,
+ workspaceId: ctx.workspace.id,
+ })),
+ )
+ .catch(() => {
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Failed to assign keys to role",
+ });
+ });
+
+ await insertAuditLogs(
+ tx,
+ keyIds.map((keyId) => ({
+ workspaceId: ctx.workspace.id,
+ event: "authorization.connect_role_and_key",
+ actor: {
+ type: "user",
+ id: ctx.user.id,
+ },
+ description: `Connected key ${keyId} and role ${roleId}`,
+ resources: [
+ { type: "key", id: keyId },
+ { type: "role", id: roleId, name: input.roleName },
+ ],
+ context: {
+ userAgent: ctx.audit.userAgent,
+ location: ctx.audit.location,
+ },
+ })),
+ );
+ }
+
await insertAuditLogs(tx, {
workspaceId: ctx.workspace.id,
event: "role.create",
@@ -171,86 +342,6 @@ export const upsertRole = t.procedure
},
});
}
-
- // Add role-permission relationships
- if (input.permissionIds && input.permissionIds.length > 0) {
- await tx
- .insert(schema.rolesPermissions)
- .values(
- input.permissionIds.map((permissionId) => ({
- permissionId,
- roleId,
- workspaceId: ctx.workspace.id,
- })),
- )
- .catch(() => {
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Failed to assign permissions to role",
- });
- });
-
- await insertAuditLogs(
- tx,
- input.permissionIds.map((permissionId) => ({
- workspaceId: ctx.workspace.id,
- event: "authorization.connect_role_and_permission",
- actor: {
- type: "user",
- id: ctx.user.id,
- },
- description: `Connected role ${roleId} and permission ${permissionId}`,
- resources: [
- { type: "role", id: roleId, name: input.roleName },
- { type: "permission", id: permissionId },
- ],
- context: {
- userAgent: ctx.audit.userAgent,
- location: ctx.audit.location,
- },
- })),
- );
- }
-
- // Add key-role relationships
- if (input.keyIds && input.keyIds.length > 0) {
- await tx
- .insert(schema.keysRoles)
- .values(
- input.keyIds.map((keyId) => ({
- keyId,
- roleId,
- workspaceId: ctx.workspace.id,
- })),
- )
- .catch(() => {
- throw new TRPCError({
- code: "INTERNAL_SERVER_ERROR",
- message: "Failed to assign keys to role",
- });
- });
-
- await insertAuditLogs(
- tx,
- input.keyIds.map((keyId) => ({
- workspaceId: ctx.workspace.id,
- event: "authorization.connect_role_and_key",
- actor: {
- type: "user",
- id: ctx.user.id,
- },
- description: `Connected key ${keyId} and role ${roleId}`,
- resources: [
- { type: "key", id: keyId },
- { type: "role", id: roleId, name: input.roleName },
- ],
- context: {
- userAgent: ctx.audit.userAgent,
- location: ctx.audit.location,
- },
- })),
- );
- }
});
return {
diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts
index e9f11a9190..e0cf974086 100644
--- a/apps/dashboard/lib/trpc/routers/index.ts
+++ b/apps/dashboard/lib/trpc/routers/index.ts
@@ -27,9 +27,11 @@ import { queryPermissions } from "./authorization/permissions/query";
import { upsertPermission } from "./authorization/permissions/upsert";
import { getConnectedKeysAndPerms } from "./authorization/roles/connected-keys-and-perms";
import { deleteRoleWithRelations } from "./authorization/roles/delete";
+import { queryRoleKeys } from "./authorization/roles/keys/connected-keys";
import { queryKeys } from "./authorization/roles/keys/query-keys";
import { searchKeys } from "./authorization/roles/keys/search-key";
import { rolesLlmSearch } from "./authorization/roles/llm-search";
+import { queryRolePermissions } from "./authorization/roles/permissions/connected-permissions";
import { queryRolesPermissions } from "./authorization/roles/permissions/query-permissions";
import { searchRolesPermissions } from "./authorization/roles/permissions/search-permissions";
import { queryRoles } from "./authorization/roles/query";
@@ -218,6 +220,8 @@ export const router = t.router({
delete: deleteRoleWithRelations,
llmSearch: rolesLlmSearch,
connectedKeysAndPerms: getConnectedKeysAndPerms,
+ connectedKeys: queryRoleKeys,
+ connectedPerms: queryRolePermissions,
}),
}),
rbac: t.router({