chore: consistent ui malformed json rejection

This commit is contained in:
Julian Sommer
2026-06-17 14:37:56 +10:00
parent 9288ecc7dc
commit b99b46c18c
12 changed files with 108 additions and 63 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1 +1 @@
import{f as e,u as t}from"./auth.js";var n=t({userName:e().min(1,`User name is required`).max(64,`User name must be 64 characters or less`).regex(/^[\w+=,.@-]+$/,`User name contains invalid characters`),path:e().optional()}),r=t({policyName:e().min(1,`Policy name is required`).max(128,`Policy name must be 128 characters or less`).regex(/^[\w+=,.@-]+$/,`Policy name contains invalid characters`),description:e().optional(),policyDocument:e().min(1,`Policy document is required`).refine(e=>{try{return JSON.parse(e),!0}catch{return!1}},{message:`Policy document must be valid JSON`})}),i=JSON.stringify({Version:`2012-10-17`,Statement:[{Effect:`Allow`,Principal:{Service:`ec2.amazonaws.com`},Action:`sts:AssumeRole`}]},null,2),a=t({roleName:e().min(1,`Role name is required`).max(64,`Role name must be 64 characters or less`).regex(/^[\w+=,.@-]+$/,`Role name contains invalid characters`),path:e().optional(),description:e().optional(),assumeRolePolicyDocument:e().min(1,`Trust policy is required`).refine(e=>{try{return JSON.parse(e),!0}catch{return!1}},{message:`Trust policy must be valid JSON`})}),o=t({instanceProfileName:e().min(1,`Instance profile name is required`).max(128,`Instance profile name must be 128 characters or less`).regex(/^[\w+=,.@-]+$/,`Instance profile name contains invalid characters`),path:e().optional()});export{n as a,a as i,o as n,r,i as t};
import{f as e,u as t}from"./auth.js";import{t as n}from"./json.js";var r=t({userName:e().min(1,`User name is required`).max(64,`User name must be 64 characters or less`).regex(/^[\w+=,.@-]+$/,`User name contains invalid characters`),path:e().optional()}),i=t({policyName:e().min(1,`Policy name is required`).max(128,`Policy name must be 128 characters or less`).regex(/^[\w+=,.@-]+$/,`Policy name contains invalid characters`),description:e().optional(),policyDocument:n({label:`Policy document`})}),a=JSON.stringify({Version:`2012-10-17`,Statement:[{Effect:`Allow`,Principal:{Service:`ec2.amazonaws.com`},Action:`sts:AssumeRole`}]},null,2),o=t({roleName:e().min(1,`Role name is required`).max(64,`Role name must be 64 characters or less`).regex(/^[\w+=,.@-]+$/,`Role name contains invalid characters`),path:e().optional(),description:e().optional(),assumeRolePolicyDocument:n({label:`Trust policy`})}),s=t({instanceProfileName:e().min(1,`Instance profile name is required`).max(128,`Instance profile name must be 128 characters or less`).regex(/^[\w+=,.@-]+$/,`Instance profile name contains invalid characters`),path:e().optional()});export{r as a,o as i,s as n,i as r,a as t};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
import{f as e}from"./auth.js";function t(e){try{return JSON.parse(e),!0}catch{return!1}}function n(n){let{label:r,allowEmpty:i=!1}=n;return(i?e():e().min(1,`${r} is required`)).refine(e=>i&&e.trim()===``?!0:t(e),{message:`${r} must be valid JSON`})}export{n as t};
@@ -17,7 +17,7 @@ vi.mock("@/lib/sts", () => ({
}))
vi.mock("@/lib/signed-fetch", () => ({
signedFetch: async (opts: unknown) => mockSignedFetch(opts),
signedFetch: async (opts: unknown) => await mockSignedFetch(opts),
}))
vi.mock("@aws-sdk/client-sts", () => ({
@@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest"
import { isValidJson, jsonStringSchema } from "./json"
type JsonSchema = ReturnType<typeof jsonStringSchema>
function issueMessages(schema: JsonSchema, value: string): string[] {
const result = schema.safeParse(value)
return result.success ? [] : result.error.issues.map((issue) => issue.message)
}
describe("isValidJson", () => {
it("accepts valid JSON", () => {
expect(isValidJson('{"a":1}')).toBeTruthy()
expect(isValidJson("[]")).toBeTruthy()
expect(isValidJson('"str"')).toBeTruthy()
})
it("rejects malformed JSON and empty strings", () => {
expect(isValidJson("{")).toBeFalsy()
expect(isValidJson("{a:1}")).toBeFalsy()
expect(isValidJson("")).toBeFalsy()
})
})
describe("jsonStringSchema", () => {
const required = jsonStringSchema({ label: "Policy document" })
const optional = jsonStringSchema({
label: "Configuration",
allowEmpty: true,
})
it("accepts valid JSON", () => {
expect(required.safeParse('{"Version":"2012-10-17"}').success).toBeTruthy()
})
it("rejects malformed JSON with a derived message", () => {
expect(issueMessages(required, "{ not json")).toContain(
"Policy document must be valid JSON",
)
})
it("rejects empty input when required", () => {
expect(issueMessages(required, "")).toContain("Policy document is required")
})
it("allows empty input when allowEmpty is set", () => {
expect(optional.safeParse("").success).toBeTruthy()
expect(optional.safeParse(" ").success).toBeTruthy()
})
it("still rejects malformed input when allowEmpty is set", () => {
expect(optional.safeParse("{bad").success).toBeFalsy()
})
})
@@ -0,0 +1,27 @@
import { z } from "zod"
export function isValidJson(value: string): boolean {
try {
JSON.parse(value)
return true
} catch {
return false
}
}
// Zod field for a JSON document entered as a string. Messages derive from
// `label` so they read naturally per field. When `allowEmpty` is true an
// empty/whitespace value passes (optional field); otherwise it is required.
export function jsonStringSchema(opts: {
label: string
allowEmpty?: boolean
}) {
const { label, allowEmpty = false } = opts
const base = allowEmpty
? z.string()
: z.string().min(1, `${label} is required`)
return base.refine(
(value) => (allowEmpty && value.trim() === "" ? true : isValidJson(value)),
{ message: `${label} must be valid JSON` },
)
}
@@ -29,6 +29,7 @@ import {
eksAddonVersionsQueryOptions,
} from "@/queries/eks"
import {
addonConfigurationSchema,
addonStatusVariant,
type CreateAddonFormValues,
createAddonSchema,
@@ -40,18 +41,6 @@ const STATUS_POLL_MS = 5000
const selectClassName =
"h-7 w-full rounded-md border border-input bg-input/20 px-2 text-sm"
function isValidJson(value: string): boolean {
if (value.trim() === "") {
return true
}
try {
JSON.parse(value)
return true
} catch {
return false
}
}
function AddAddonDialog({
clusterName,
catalog,
@@ -257,10 +246,14 @@ function UpdateAddonDialog({
])
const handleConfirm = () => {
if (!isValidJson(config)) {
setError("Configuration must be valid JSON")
const result = addonConfigurationSchema.safeParse(config)
if (!result.success) {
setError(
result.error.issues[0]?.message ?? "Configuration must be valid JSON",
)
return
}
setError(undefined)
updateAddon.mutate(
{
clusterName,
@@ -1,6 +1,7 @@
import type { AMITypes, CapacityTypes } from "@aws-sdk/client-eks"
import { z } from "zod"
import { jsonStringSchema } from "@/lib/json"
import { isValidCidr } from "@/lib/subnet-calculator"
// K3s control plane is pinned per Mulga release; expose the supported
@@ -89,24 +90,16 @@ export const createNodegroupSchema = z
export type CreateNodegroupFormValues = z.infer<typeof createNodegroupSchema>
export const addonConfigurationSchema = jsonStringSchema({
label: "Configuration",
allowEmpty: true,
})
export const createAddonSchema = z.object({
addonName: z.string().min(1, "Add-on is required"),
addonVersion: z.string().min(1, "Version is required"),
serviceAccountRoleArn: z.string(),
configurationValues: z.string().refine(
(v) => {
if (v.trim() === "") {
return true
}
try {
JSON.parse(v)
return true
} catch {
return false
}
},
{ message: "Configuration must be valid JSON" },
),
configurationValues: addonConfigurationSchema,
})
export type CreateAddonFormValues = z.infer<typeof createAddonSchema>
@@ -1,5 +1,7 @@
import { z } from "zod"
import { jsonStringSchema } from "@/lib/json"
export const createUserSchema = z.object({
userName: z
.string()
@@ -18,20 +20,7 @@ export const createPolicySchema = z.object({
.max(128, "Policy name must be 128 characters or less")
.regex(/^[\w+=,.@-]+$/, "Policy name contains invalid characters"),
description: z.string().optional(),
policyDocument: z
.string()
.min(1, "Policy document is required")
.refine(
(val) => {
try {
JSON.parse(val)
return true
} catch {
return false
}
},
{ message: "Policy document must be valid JSON" },
),
policyDocument: jsonStringSchema({ label: "Policy document" }),
})
export type CreatePolicyFormData = z.infer<typeof createPolicySchema>
@@ -59,20 +48,7 @@ export const createRoleSchema = z.object({
.regex(/^[\w+=,.@-]+$/, "Role name contains invalid characters"),
path: z.string().optional(),
description: z.string().optional(),
assumeRolePolicyDocument: z
.string()
.min(1, "Trust policy is required")
.refine(
(val) => {
try {
JSON.parse(val)
return true
} catch {
return false
}
},
{ message: "Trust policy must be valid JSON" },
),
assumeRolePolicyDocument: jsonStringSchema({ label: "Trust policy" }),
})
export type CreateRoleFormData = z.infer<typeof createRoleSchema>