mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-08 01:09:51 +00:00
🚧 WIP
This commit is contained in:
@@ -766,6 +766,12 @@
|
||||
"resourcePincodeSetupTitle": "Set Pincode",
|
||||
"resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource",
|
||||
"resourceRoleDescription": "Admins can always access this resource.",
|
||||
"resourcePolicySelectTitle": "Resource Access Policy",
|
||||
"resourcePolicySelectDescription": "Select the resource policy type for authentication",
|
||||
"resourcePolicyInline": "Inline Resource Policy",
|
||||
"resourcePolicyInlineDescription": "Access Policy scoped to only this resource",
|
||||
"resourcePolicyShared": "Shared Resource Policy",
|
||||
"resourcePolicySharedDescription": "Access Policy shared accross multiple resources",
|
||||
"resourceUsersRoles": "Access Controls",
|
||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||
|
||||
@@ -146,7 +146,8 @@ export enum ActionsEnum {
|
||||
setResourcePolicyPincode = "setResourcePolicyPincode",
|
||||
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
|
||||
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
|
||||
setResourcePolicyRules = "setResourcePolicyRules"
|
||||
setResourcePolicyRules = "setResourcePolicyRules",
|
||||
getResourcePolicies = "getResourcePolicies"
|
||||
}
|
||||
|
||||
export async function checkUserActionPermission(
|
||||
|
||||
@@ -636,6 +636,13 @@ authenticated.get(
|
||||
policy.getResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
verifyResourceAccess,
|
||||
verifyUserHasAction(ActionsEnum.getResourcePolicies),
|
||||
resource.getResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.put(
|
||||
"/resource-policy/:resourcePolicyId",
|
||||
verifyResourcePolicyAccess,
|
||||
|
||||
@@ -453,6 +453,13 @@ authenticated.get(
|
||||
policy.getResourcePolicy
|
||||
);
|
||||
|
||||
authenticated.get(
|
||||
"/resource/:resourceId/policies",
|
||||
verifyApiKeyResourceAccess,
|
||||
verifyApiKeyHasAction(ActionsEnum.getResourcePolicies),
|
||||
resource.getResourcePolicies
|
||||
);
|
||||
|
||||
authenticated.post(
|
||||
"/resource/:resourceId",
|
||||
verifyApiKeyResourceAccess,
|
||||
|
||||
@@ -40,7 +40,9 @@ const getResourcePolicySchema = z
|
||||
})
|
||||
);
|
||||
|
||||
async function query(params: z.infer<typeof getResourcePolicySchema>) {
|
||||
export async function queryResourcePolicy(
|
||||
params: z.infer<typeof getResourcePolicySchema>
|
||||
) {
|
||||
const conditions: SQL<unknown>[] = [];
|
||||
if ("resourcePolicyId" in params) {
|
||||
conditions.push(
|
||||
@@ -158,7 +160,7 @@ async function query(params: z.infer<typeof getResourcePolicySchema>) {
|
||||
}
|
||||
|
||||
export type GetResourcePolicyResponse = NonNullable<
|
||||
Awaited<ReturnType<typeof query>>
|
||||
Awaited<ReturnType<typeof queryResourcePolicy>>
|
||||
>;
|
||||
|
||||
registry.registerPath({
|
||||
@@ -205,7 +207,7 @@ export async function getResourcePolicy(
|
||||
);
|
||||
}
|
||||
|
||||
const policy = await query(parsedParams.data);
|
||||
const policy = await queryResourcePolicy(parsedParams.data);
|
||||
|
||||
if (!policy) {
|
||||
return next(
|
||||
|
||||
88
server/routers/resource/getResourcePolicies.ts
Normal file
88
server/routers/resource/getResourcePolicies.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { db, resources } from "@server/db";
|
||||
import {
|
||||
queryResourcePolicy,
|
||||
type GetResourcePolicyResponse
|
||||
} from "@server/routers/policy/getResourcePolicy";
|
||||
import response from "@server/lib/response";
|
||||
import logger from "@server/logger";
|
||||
import { OpenAPITags, registry } from "@server/openApi";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import z from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
const getResourcePoliciesParamsSchema = z.strictObject({
|
||||
resourceId: z.string().transform(Number).pipe(z.int().positive())
|
||||
});
|
||||
|
||||
export type GetResourcePoliciesResponse = {
|
||||
defaultPolicy: GetResourcePolicyResponse | null;
|
||||
};
|
||||
|
||||
registry.registerPath({
|
||||
method: "get",
|
||||
path: "/resource/{resourceId}/policies",
|
||||
description: "Get the default policy for a resource.",
|
||||
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy],
|
||||
request: {
|
||||
params: getResourcePoliciesParamsSchema
|
||||
},
|
||||
responses: {}
|
||||
});
|
||||
|
||||
export async function getResourcePolicies(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Promise<any> {
|
||||
try {
|
||||
const parsedParams = getResourcePoliciesParamsSchema.safeParse(
|
||||
req.params
|
||||
);
|
||||
if (!parsedParams.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedParams.error).toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const { resourceId } = parsedParams.data;
|
||||
|
||||
const [resource] = await db
|
||||
.select({
|
||||
defaultResourcePolicyId: resources.defaultResourcePolicyId
|
||||
})
|
||||
.from(resources)
|
||||
.where(eq(resources.resourceId, resourceId))
|
||||
.limit(1);
|
||||
|
||||
if (!resource) {
|
||||
return next(
|
||||
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
|
||||
);
|
||||
}
|
||||
|
||||
const defaultPolicy = resource.defaultResourcePolicyId
|
||||
? await queryResourcePolicy({
|
||||
resourcePolicyId: resource.defaultResourcePolicyId
|
||||
})
|
||||
: null;
|
||||
|
||||
return response<GetResourcePoliciesResponse>(res, {
|
||||
data: { defaultPolicy },
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Resource policies retrieved successfully",
|
||||
status: HttpCode.OK
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return next(
|
||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,3 +31,4 @@ export * from "./addUserToResource";
|
||||
export * from "./removeUserFromResource";
|
||||
export * from "./listAllResourceNames";
|
||||
export * from "./removeEmailFromResourceWhitelist";
|
||||
export * from "./getResourcePolicies";
|
||||
|
||||
3
solo.yml
Normal file
3
solo.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: pangolin
|
||||
icon: public/logo/pangolin_profile_picture.png
|
||||
processes: {}
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import {
|
||||
StrategySelect,
|
||||
type StrategyOption
|
||||
} from "@app/components/StrategySelect";
|
||||
import { SwitchInput } from "@app/components/SwitchInput";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
@@ -42,6 +46,7 @@ import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
|
||||
import { orgQueries, resourceQueries } from "@app/lib/queries";
|
||||
import { ResourcePolicyContext } from "@app/providers/ResourcePolicyProvider";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { build } from "@server/build";
|
||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||
@@ -86,6 +91,8 @@ const whitelistSchema = z.object({
|
||||
)
|
||||
});
|
||||
|
||||
type ResourcePolicyType = StrategyOption<"inline" | "shared">;
|
||||
|
||||
export default function ResourceAuthenticationPage() {
|
||||
const { org } = useOrgContext();
|
||||
const { resource, updateResource, authInfo, updateAuthInfo } =
|
||||
@@ -118,6 +125,11 @@ export default function ResourceAuthenticationPage() {
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
const { data: policies, isLoading: isLoadingPolicies } = useQuery(
|
||||
resourceQueries.policies({
|
||||
resourceId: resource.resourceId
|
||||
})
|
||||
);
|
||||
|
||||
const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery(
|
||||
orgQueries.roles({
|
||||
@@ -142,7 +154,8 @@ export default function ResourceAuthenticationPage() {
|
||||
isLoadingResourceRoles ||
|
||||
isLoadingResourceUsers ||
|
||||
isLoadingWhiteList ||
|
||||
isLoadingOrgIdps;
|
||||
isLoadingOrgIdps ||
|
||||
isLoadingPolicies;
|
||||
|
||||
const allRoles = useMemo(() => {
|
||||
return orgRoles
|
||||
@@ -413,6 +426,22 @@ export default function ResourceAuthenticationPage() {
|
||||
.finally(() => setLoadingRemoveResourceHeaderAuth(false));
|
||||
}
|
||||
|
||||
const resourcePolicyTypes: Array<ResourcePolicyType> = [
|
||||
{
|
||||
id: "inline",
|
||||
title: t("resourcePolicyInline"),
|
||||
description: t("resourcePolicyInlineDescription")
|
||||
},
|
||||
{
|
||||
id: "shared",
|
||||
title: t("resourcePolicyShared"),
|
||||
description: t("resourcePolicySharedDescription")
|
||||
}
|
||||
];
|
||||
|
||||
const [selectedResourceType, setSelectedResourceType] =
|
||||
useState<ResourcePolicyType["id"]>("inline");
|
||||
|
||||
if (pageLoading) {
|
||||
return <></>;
|
||||
}
|
||||
@@ -465,324 +494,39 @@ export default function ResourceAuthenticationPage() {
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceUsersRoles")}
|
||||
{t("resourcePolicySelectTitle")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceUsersRolesDescription")}
|
||||
{t("resourcePolicySelectDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<SwitchInput
|
||||
id="sso-toggle"
|
||||
label={t("ssoUse")}
|
||||
checked={ssoEnabled}
|
||||
onCheckedChange={(val) => setSsoEnabled(val)}
|
||||
/>
|
||||
|
||||
<Form {...usersRolesForm}>
|
||||
<form
|
||||
action={submitUserRolesForm}
|
||||
id="users-roles-form"
|
||||
className="space-y-4"
|
||||
>
|
||||
{ssoEnabled && (
|
||||
<>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="roles"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>
|
||||
{t("roles")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeRolesTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveRolesTagIndex
|
||||
}
|
||||
placeholder={t(
|
||||
"accessRoleSelect2"
|
||||
)}
|
||||
size="sm"
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.roles
|
||||
}
|
||||
setTags={(
|
||||
newRoles
|
||||
) => {
|
||||
usersRolesForm.setValue(
|
||||
"roles",
|
||||
newRoles as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={
|
||||
true
|
||||
}
|
||||
autocompleteOptions={
|
||||
allRoles
|
||||
}
|
||||
allowDuplicates={
|
||||
false
|
||||
}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
<FormDescription>
|
||||
{t(
|
||||
"resourceRoleDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={usersRolesForm.control}
|
||||
name="users"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>
|
||||
{t("users")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
activeUsersTagIndex
|
||||
}
|
||||
setActiveTagIndex={
|
||||
setActiveUsersTagIndex
|
||||
}
|
||||
placeholder={t(
|
||||
"accessUserSelect"
|
||||
)}
|
||||
tags={
|
||||
usersRolesForm.getValues()
|
||||
.users
|
||||
}
|
||||
size="sm"
|
||||
setTags={(
|
||||
newUsers
|
||||
) => {
|
||||
usersRolesForm.setValue(
|
||||
"users",
|
||||
newUsers as [
|
||||
Tag,
|
||||
...Tag[]
|
||||
]
|
||||
);
|
||||
}}
|
||||
enableAutocomplete={
|
||||
true
|
||||
}
|
||||
autocompleteOptions={
|
||||
allUsers
|
||||
}
|
||||
allowDuplicates={
|
||||
false
|
||||
}
|
||||
restrictTagsToAutocompleteOptions={
|
||||
true
|
||||
}
|
||||
sortTags={true}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{ssoEnabled && allIdps.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
{t("defaultIdentityProvider")}
|
||||
</label>
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
if (value === "none") {
|
||||
setSelectedIdpId(null);
|
||||
} else {
|
||||
setSelectedIdpId(
|
||||
parseInt(value)
|
||||
);
|
||||
}
|
||||
}}
|
||||
value={
|
||||
selectedIdpId
|
||||
? selectedIdpId.toString()
|
||||
: "none"
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-full mt-1">
|
||||
<SelectValue
|
||||
placeholder={t(
|
||||
"selectIdpPlaceholder"
|
||||
)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="none">
|
||||
{t("none")}
|
||||
</SelectItem>
|
||||
{allIdps.map((idp) => (
|
||||
<SelectItem
|
||||
key={idp.id}
|
||||
value={idp.id.toString()}
|
||||
>
|
||||
{idp.text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
"defaultIdentityProviderDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
<StrategySelect
|
||||
options={resourcePolicyTypes}
|
||||
defaultValue="inline"
|
||||
onChange={(value) => {
|
||||
// baseForm.setValue(
|
||||
// "http",
|
||||
// value === "http"
|
||||
// );
|
||||
// // Update method default when switching resource type
|
||||
}}
|
||||
cols={2}
|
||||
/>
|
||||
</SettingsSectionBody>
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
loading={loadingSaveUsersRoles}
|
||||
disabled={loadingSaveUsersRoles}
|
||||
form="users-roles-form"
|
||||
disabled
|
||||
form="policies-type-form"
|
||||
>
|
||||
{t("resourceUsersRolesSubmit")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
</SettingsSection>
|
||||
{/* <ResourcePolicyContext value={policies?.defaultPolicy}>
|
||||
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("resourceAuthMethods")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("resourceAuthMethodsDescriptions")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
{/* Password Protection */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2 mb-4">
|
||||
<div
|
||||
className={`flex items-center ${!authInfo.password ? "" : "text-green-500"} text-sm space-x-2`}
|
||||
>
|
||||
<Key size="14" />
|
||||
<span>
|
||||
{t("resourcePasswordProtection", {
|
||||
status: authInfo.password
|
||||
? t("enabled")
|
||||
: t("disabled")
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
authInfo.password
|
||||
? removeResourcePassword
|
||||
: () => setIsSetPasswordOpen(true)
|
||||
}
|
||||
loading={loadingRemoveResourcePassword}
|
||||
>
|
||||
{authInfo.password
|
||||
? t("passwordRemove")
|
||||
: t("passwordAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* PIN Code Protection */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={`flex items-center ${!authInfo.pincode ? "" : "text-green-500"} space-x-2 text-sm`}
|
||||
>
|
||||
<Binary size="14" />
|
||||
<span>
|
||||
{t("resourcePincodeProtection", {
|
||||
status: authInfo.pincode
|
||||
? t("enabled")
|
||||
: t("disabled")
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
authInfo.pincode
|
||||
? removeResourcePincode
|
||||
: () => setIsSetPincodeOpen(true)
|
||||
}
|
||||
loading={loadingRemoveResourcePincode}
|
||||
>
|
||||
{authInfo.pincode
|
||||
? t("pincodeRemove")
|
||||
: t("pincodeAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Header Authentication Protection */}
|
||||
<div className="flex items-center justify-between border rounded-md p-2">
|
||||
<div
|
||||
className={`flex items-center ${!authInfo.headerAuth ? "" : "text-green-500"} space-x-2 text-sm`}
|
||||
>
|
||||
<Bot size="14" />
|
||||
<span>
|
||||
{authInfo.headerAuth
|
||||
? t(
|
||||
"resourceHeaderAuthProtectionEnabled"
|
||||
)
|
||||
: t(
|
||||
"resourceHeaderAuthProtectionDisabled"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={
|
||||
authInfo.headerAuth
|
||||
? removeResourceHeaderAuth
|
||||
: () => setIsSetHeaderAuthOpen(true)
|
||||
}
|
||||
loading={loadingRemoveResourceHeaderAuth}
|
||||
>
|
||||
{authInfo.headerAuth
|
||||
? t("headerAuthRemove")
|
||||
: t("headerAuthAdd")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
<OneTimePasswordFormSection
|
||||
resource={resource}
|
||||
updateResource={updateResource}
|
||||
whitelist={whitelist}
|
||||
isLoadingWhiteList={isLoadingWhiteList}
|
||||
/>
|
||||
</ResourcePolicyContext> */}
|
||||
</SettingsContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -25,11 +25,15 @@ export function StrategySelect<TValue extends string>({
|
||||
value: controlledValue,
|
||||
defaultValue,
|
||||
onChange,
|
||||
cols
|
||||
cols = 1
|
||||
}: StrategySelectProps<TValue>) {
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<TValue | undefined>(defaultValue);
|
||||
const [uncontrolledSelected, setUncontrolledSelected] = useState<
|
||||
TValue | undefined
|
||||
>(defaultValue);
|
||||
const isControlled = controlledValue !== undefined;
|
||||
const selected = isControlled ? (controlledValue ?? undefined) : uncontrolledSelected;
|
||||
const selected = isControlled
|
||||
? (controlledValue ?? undefined)
|
||||
: uncontrolledSelected;
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
@@ -39,7 +43,11 @@ export function StrategySelect<TValue extends string>({
|
||||
if (!isControlled) setUncontrolledSelected(typedValue);
|
||||
onChange?.(typedValue);
|
||||
}}
|
||||
className={`grid md:grid-cols-${cols ? cols : 1} gap-4`}
|
||||
style={{
|
||||
// @ts-expect-error
|
||||
"--cols": `repeat(${cols}, 1fr)`
|
||||
}}
|
||||
className="grid md:grid-cols-(--cols) gap-4"
|
||||
>
|
||||
{options.map((option: StrategyOption<TValue>) => (
|
||||
<label
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ListClientsResponse } from "@server/routers/client";
|
||||
import type { ListDomainsResponse } from "@server/routers/domain";
|
||||
import type {
|
||||
GetResourceWhitelistResponse,
|
||||
GetResourcePoliciesResponse,
|
||||
ListResourceNamesResponse,
|
||||
ListResourcesResponse,
|
||||
ListResourceRolesResponse,
|
||||
@@ -322,6 +323,17 @@ export const resourceQueries = {
|
||||
return res.data.data.whitelist;
|
||||
}
|
||||
}),
|
||||
policies: ({ resourceId }: { resourceId: number }) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES", resourceId, "POLICIES"] as const,
|
||||
queryFn: async ({ signal, meta }) => {
|
||||
const res = await meta!.api.get<
|
||||
AxiosResponse<GetResourcePoliciesResponse>
|
||||
>(`/resource/${resourceId}/policies`, { signal });
|
||||
|
||||
return res.data.data;
|
||||
}
|
||||
}),
|
||||
listNamesPerOrg: (orgId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ["RESOURCES_NAMES", orgId] as const,
|
||||
|
||||
Reference in New Issue
Block a user