mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 13:26:16 +00:00
Merge pull request #3196 from Infisical/org-name-constraint
Improvement: Add Organization Name Constraint
This commit is contained in:
@@ -21,3 +21,10 @@ export const slugSchema = ({ min = 1, max = 32, field = "Slug" }: SlugSchemaInpu
|
|||||||
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
message: `${field} field can only contain lowercase letters, numbers, and hyphens`
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GenericResourceNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Name must be at least 1 character" })
|
||||||
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
|
.regex(/^[a-zA-Z0-9\-_\s]+$/, "Name can only contain alphanumeric characters, dashes, underscores, and spaces");
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-t
|
|||||||
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
import { AUDIT_LOGS, ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
import { getLastMidnightDateISO, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { slugSchema } from "@app/server/lib/schemas";
|
import { GenericResourceNameSchema, slugSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode, MfaMethod } from "@app/services/auth/auth-type";
|
||||||
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
import { sanitizedOrganizationSchema } from "@app/services/org/org-schema";
|
||||||
@@ -251,7 +251,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
schema: {
|
schema: {
|
||||||
params: z.object({ organizationId: z.string().trim() }),
|
params: z.object({ organizationId: z.string().trim() }),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().max(64, { message: "Name must be 64 or fewer characters" }).optional(),
|
name: GenericResourceNameSchema.optional(),
|
||||||
slug: slugSchema({ max: 64 }).optional(),
|
slug: slugSchema({ max: 64 }).optional(),
|
||||||
authEnforced: z.boolean().optional(),
|
authEnforced: z.boolean().optional(),
|
||||||
scimEnabled: z.boolean().optional(),
|
scimEnabled: z.boolean().optional(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
import { ORGANIZATIONS } from "@app/lib/api-docs";
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -330,7 +331,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim()
|
name: GenericResourceNameSchema
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { UsersSchema } from "@app/db/schemas";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { ForbiddenRequestError } from "@app/lib/errors";
|
import { ForbiddenRequestError } from "@app/lib/errors";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { GenericResourceNameSchema } from "@app/server/lib/schemas";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||||
|
|
||||||
@@ -100,7 +101,7 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
|
|||||||
encryptedPrivateKeyTag: z.string().trim(),
|
encryptedPrivateKeyTag: z.string().trim(),
|
||||||
salt: z.string().trim(),
|
salt: z.string().trim(),
|
||||||
verifier: z.string().trim(),
|
verifier: z.string().trim(),
|
||||||
organizationName: z.string().trim().min(1),
|
organizationName: GenericResourceNameSchema,
|
||||||
providerAuthToken: z.string().trim().optional().nullish(),
|
providerAuthToken: z.string().trim().optional().nullish(),
|
||||||
attributionSource: z.string().trim().optional(),
|
attributionSource: z.string().trim().optional(),
|
||||||
password: z.string()
|
password: z.string()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { encodeBase64 } from "tweetnacl-util";
|
|||||||
import { initProjectHelper } from "@app/helpers/project";
|
import { initProjectHelper } from "@app/helpers/project";
|
||||||
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
|
||||||
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
import { fetchOrganizations } from "@app/hooks/api/organization/queries";
|
||||||
|
import { onRequestError } from "@app/hooks/api/reactQuery";
|
||||||
|
|
||||||
import InputField from "../basic/InputField";
|
import InputField from "../basic/InputField";
|
||||||
import checkPassword from "../utilities/checks/password/checkPassword";
|
import checkPassword from "../utilities/checks/password/checkPassword";
|
||||||
@@ -206,6 +207,7 @@ export default function UserInfoStep({
|
|||||||
|
|
||||||
incrementStep();
|
incrementStep();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
onRequestError(error);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { createNotification } from "@app/components/notifications";
|
|||||||
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
import { Button, FormControl, Input, Modal, ModalContent } from "@app/components/v2";
|
||||||
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
import { useCreateOrg, useSelectOrganization } from "@app/hooks/api";
|
||||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||||
|
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().nonempty({ message: "Name is required" })
|
name: GenericResourceNameSchema.nonempty({ message: "Name is required" })
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export const CreateOrgModal: FC<CreateOrgModalProps> = ({ isOpen, onClose }) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen}>
|
<Modal modal={false} isOpen={isOpen}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Create Organization"
|
title="Create Organization"
|
||||||
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
subTitle="Looks like you're not part of any organizations. Create one to start using Infisical"
|
||||||
|
|||||||
@@ -18,268 +18,44 @@ export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"];
|
|||||||
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
export const MFA_TEMP_TOKEN_CACHE_KEY = ["infisical__mfa-temp-token"];
|
||||||
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
export const AUTH_TOKEN_CACHE_KEY = ["infisical__auth-token"];
|
||||||
|
|
||||||
export const queryClient = new QueryClient({
|
export const onRequestError = (error: unknown) => {
|
||||||
mutationCache: new MutationCache({
|
if (axios.isAxiosError(error)) {
|
||||||
onError: (error) => {
|
const serverResponse = error.response?.data as TApiErrors;
|
||||||
if (axios.isAxiosError(error)) {
|
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
||||||
const serverResponse = error.response?.data as TApiErrors;
|
createNotification(
|
||||||
if (serverResponse?.error === ApiErrorTypes.ValidationError) {
|
{
|
||||||
createNotification(
|
title: "Validation Error",
|
||||||
{
|
|
||||||
title: "Validation Error",
|
|
||||||
type: "error",
|
|
||||||
text: "Please check the input and try again.",
|
|
||||||
callToAction: (
|
|
||||||
<Modal>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button variant="outline_bg" size="xs">
|
|
||||||
Show more
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent title="Validation Error Details">
|
|
||||||
<TableContainer>
|
|
||||||
<Table>
|
|
||||||
<THead>
|
|
||||||
<Tr>
|
|
||||||
<Th>Field</Th>
|
|
||||||
<Th>Issue</Th>
|
|
||||||
</Tr>
|
|
||||||
</THead>
|
|
||||||
<TBody>
|
|
||||||
{serverResponse.message?.map(({ message, path }) => (
|
|
||||||
<Tr key={path.join(".")}>
|
|
||||||
<Td>{path.join(".")}</Td>
|
|
||||||
<Td>{message.toLowerCase()}</Td>
|
|
||||||
</Tr>
|
|
||||||
))}
|
|
||||||
</TBody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
),
|
|
||||||
copyActions: [
|
|
||||||
{
|
|
||||||
value: serverResponse.reqId,
|
|
||||||
name: "Request ID",
|
|
||||||
label: `Request ID: ${serverResponse.reqId}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ closeOnClick: false }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (serverResponse?.error === ApiErrorTypes.PermissionBoundaryError) {
|
|
||||||
createNotification(
|
|
||||||
{
|
|
||||||
title: "Forbidden Access",
|
|
||||||
type: "error",
|
|
||||||
text: `${serverResponse.message}.`,
|
|
||||||
callToAction: serverResponse?.details?.missingPermissions?.length ? (
|
|
||||||
<Modal>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button variant="outline_bg" size="xs">
|
|
||||||
Show more
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent title="Missing Permission">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{serverResponse.details?.missingPermissions?.map((el, index) => {
|
|
||||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`Forbidden-error-details-${index + 1}`}
|
|
||||||
className="rounded-md border border-gray-600 p-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
You are not authorized to perform the <b>{el.action}</b> action on the{" "}
|
|
||||||
<b>{el.subject}</b> resource.{" "}
|
|
||||||
{hasConditions &&
|
|
||||||
"Your permission does not allow access to the following conditions:"}
|
|
||||||
</div>
|
|
||||||
{hasConditions && (
|
|
||||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
|
||||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
|
||||||
const operators = (
|
|
||||||
el.conditions as Record<
|
|
||||||
string,
|
|
||||||
| string
|
|
||||||
| { [K in PermissionConditionOperators]: string | string[] }
|
|
||||||
>
|
|
||||||
)[field];
|
|
||||||
|
|
||||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
|
||||||
if (typeof operators === "string") {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={`Forbidden-error-details-${index + 1}-${
|
|
||||||
fieldIndex + 1
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="font-bold capitalize">
|
|
||||||
{formattedFieldName}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
|
||||||
<span className="text-yellow-600">{operators}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
|
||||||
<li
|
|
||||||
key={`Forbidden-error-details-${index + 1}-${
|
|
||||||
fieldIndex + 1
|
|
||||||
}-${operatorIndex + 1}`}
|
|
||||||
>
|
|
||||||
<span className="font-bold capitalize">
|
|
||||||
{formattedFieldName}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-mineshaft-200">
|
|
||||||
{
|
|
||||||
formatedConditionsOperatorNames[
|
|
||||||
operator as PermissionConditionOperators
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-yellow-600">
|
|
||||||
{operators[
|
|
||||||
operator as PermissionConditionOperators
|
|
||||||
].toString()}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
));
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
) : undefined,
|
|
||||||
copyActions: [
|
|
||||||
{
|
|
||||||
value: serverResponse.reqId,
|
|
||||||
name: "Request ID",
|
|
||||||
label: `Request ID: ${serverResponse.reqId}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ closeOnClick: false }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
|
||||||
createNotification(
|
|
||||||
{
|
|
||||||
title: "Forbidden Access",
|
|
||||||
type: "error",
|
|
||||||
text: `${serverResponse.message}.`,
|
|
||||||
callToAction: serverResponse?.details?.length ? (
|
|
||||||
<Modal>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button variant="outline_bg" size="xs">
|
|
||||||
Show more
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
<ModalContent
|
|
||||||
title="Validation Rules"
|
|
||||||
subTitle="Please review the allowed rules below."
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{serverResponse.details?.map((el, index) => {
|
|
||||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`Forbidden-error-details-${index + 1}`}
|
|
||||||
className="rounded-md border border-gray-600 p-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{el.inverted ? "Cannot" : "Can"}{" "}
|
|
||||||
<span className="text-yellow-600">
|
|
||||||
{el.action.toString().replaceAll(",", ", ")}
|
|
||||||
</span>{" "}
|
|
||||||
{el.subject.toString()} {hasConditions && "with conditions:"}
|
|
||||||
</div>
|
|
||||||
{hasConditions && (
|
|
||||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
|
||||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
|
||||||
const operators = (
|
|
||||||
el.conditions as Record<
|
|
||||||
string,
|
|
||||||
| string
|
|
||||||
| { [K in PermissionConditionOperators]: string | string[] }
|
|
||||||
>
|
|
||||||
)[field];
|
|
||||||
|
|
||||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
|
||||||
if (typeof operators === "string") {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={`Forbidden-error-details-${index + 1}-${
|
|
||||||
fieldIndex + 1
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="font-bold capitalize">
|
|
||||||
{formattedFieldName}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
|
||||||
<span className="text-yellow-600">{operators}</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
|
||||||
<li
|
|
||||||
key={`Forbidden-error-details-${index + 1}-${
|
|
||||||
fieldIndex + 1
|
|
||||||
}-${operatorIndex + 1}`}
|
|
||||||
>
|
|
||||||
<span className="font-bold capitalize">
|
|
||||||
{formattedFieldName}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-mineshaft-200">
|
|
||||||
{
|
|
||||||
formatedConditionsOperatorNames[
|
|
||||||
operator as PermissionConditionOperators
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</span>{" "}
|
|
||||||
<span className="text-yellow-600">
|
|
||||||
{operators[
|
|
||||||
operator as PermissionConditionOperators
|
|
||||||
].toString()}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
));
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
) : undefined,
|
|
||||||
copyActions: [
|
|
||||||
{
|
|
||||||
value: serverResponse.reqId,
|
|
||||||
name: "Request ID",
|
|
||||||
label: `Request ID: ${serverResponse.reqId}`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ closeOnClick: false }
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createNotification({
|
|
||||||
title: "Bad Request",
|
|
||||||
type: "error",
|
type: "error",
|
||||||
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
|
text: "Please check the input and try again.",
|
||||||
|
callToAction: (
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<Button variant="outline_bg" size="xs">
|
||||||
|
Show more
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent title="Validation Error Details">
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Field</Th>
|
||||||
|
<Th>Issue</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{serverResponse.message?.map(({ message, path }) => (
|
||||||
|
<Tr key={path.join(".")}>
|
||||||
|
<Td>{path.join(".")}</Td>
|
||||||
|
<Td>{message.toLowerCase()}</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
),
|
||||||
copyActions: [
|
copyActions: [
|
||||||
{
|
{
|
||||||
value: serverResponse.reqId,
|
value: serverResponse.reqId,
|
||||||
@@ -287,9 +63,128 @@ export const queryClient = new QueryClient({
|
|||||||
label: `Request ID: ${serverResponse.reqId}`
|
label: `Request ID: ${serverResponse.reqId}`
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
},
|
||||||
}
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||||
|
createNotification(
|
||||||
|
{
|
||||||
|
title: "Forbidden Access",
|
||||||
|
type: "error",
|
||||||
|
text: `${serverResponse.message}.`,
|
||||||
|
callToAction: serverResponse?.details?.length ? (
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<Button variant="outline_bg" size="xs">
|
||||||
|
Show more
|
||||||
|
</Button>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Validation Rules"
|
||||||
|
subTitle="Please review the allowed rules below."
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{serverResponse.details?.map((el, index) => {
|
||||||
|
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`Forbidden-error-details-${index + 1}`}
|
||||||
|
className="rounded-md border border-gray-600 p-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{el.inverted ? "Cannot" : "Can"}{" "}
|
||||||
|
<span className="text-yellow-600">
|
||||||
|
{el.action.toString().replaceAll(",", ", ")}
|
||||||
|
</span>{" "}
|
||||||
|
{el.subject.toString()} {hasConditions && "with conditions:"}
|
||||||
|
</div>
|
||||||
|
{hasConditions && (
|
||||||
|
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||||
|
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||||
|
const operators = (
|
||||||
|
el.conditions as Record<
|
||||||
|
string,
|
||||||
|
| string
|
||||||
|
| { [K in PermissionConditionOperators]: string | string[] }
|
||||||
|
>
|
||||||
|
)[field];
|
||||||
|
|
||||||
|
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||||
|
if (typeof operators === "string") {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${fieldIndex + 1}`}
|
||||||
|
>
|
||||||
|
<span className="font-bold capitalize">
|
||||||
|
{formattedFieldName}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||||
|
<span className="text-yellow-600">{operators}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||||
|
<li
|
||||||
|
key={`Forbidden-error-details-${index + 1}-${
|
||||||
|
fieldIndex + 1
|
||||||
|
}-${operatorIndex + 1}`}
|
||||||
|
>
|
||||||
|
<span className="font-bold capitalize">{formattedFieldName}</span>{" "}
|
||||||
|
<span className="text-mineshaft-200">
|
||||||
|
{
|
||||||
|
formatedConditionsOperatorNames[
|
||||||
|
operator as PermissionConditionOperators
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</span>{" "}
|
||||||
|
<span className="text-yellow-600">
|
||||||
|
{operators[operator as PermissionConditionOperators].toString()}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
) : undefined,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: serverResponse.reqId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${serverResponse.reqId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ closeOnClick: false }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
createNotification({
|
||||||
|
title: "Bad Request",
|
||||||
|
type: "error",
|
||||||
|
text: `${serverResponse.message}${serverResponse.message?.endsWith(".") ? "" : "."}`,
|
||||||
|
copyActions: [
|
||||||
|
{
|
||||||
|
value: serverResponse.reqId,
|
||||||
|
name: "Request ID",
|
||||||
|
label: `Request ID: ${serverResponse.reqId}`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient({
|
||||||
|
mutationCache: new MutationCache({
|
||||||
|
onError: onRequestError
|
||||||
}),
|
}),
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
queries: {
|
queries: {
|
||||||
|
|||||||
@@ -1 +1,13 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export * from "./slugSchema";
|
export * from "./slugSchema";
|
||||||
|
|
||||||
|
export const GenericResourceNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, { message: "Name must be at least 1 character" })
|
||||||
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9\-_\s]+$/,
|
||||||
|
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
|
||||||
|
);
|
||||||
|
|||||||
+2
-1
@@ -14,9 +14,10 @@ import {
|
|||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { isCustomOrgRole } from "@app/helpers/roles";
|
import { isCustomOrgRole } from "@app/helpers/roles";
|
||||||
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
import { useGetOrgRoles, useUpdateOrg } from "@app/hooks/api";
|
||||||
|
import { GenericResourceNameSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
name: z.string().max(64, "Too long, maximum length is 64 characters"),
|
name: GenericResourceNameSchema,
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens"),
|
.regex(/^[a-zA-Z0-9-]+$/, "Name must only contain alphanumeric characters or hyphens"),
|
||||||
|
|||||||
Reference in New Issue
Block a user