diff --git a/README.md b/README.md index 982ae140..1d21b8c8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ _Pangolin tunnels your services to the internet so you can access anything from [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) [![Discord](https://img.shields.io/discord/1325658630518865980?logo=discord&style=flat-square)](https://discord.gg/HCJR8Xhme4) -[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) +[![Youtube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) diff --git a/blueprint.py b/blueprint.py index 93d21da5..b4f1a99c 100644 --- a/blueprint.py +++ b/blueprint.py @@ -8,10 +8,10 @@ import base64 YAML_FILE_PATH = 'blueprint.yaml' # The API endpoint and headers from the curl request -API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint' +API_URL = 'http://localhost:3004/v1/org/test/blueprint' HEADERS = { 'accept': '*/*', - 'Authorization': 'Bearer ', + 'Authorization': 'Bearer v7ix7xha1bmq2on.tzsden374mtmkeczm3tx44uzxsljnrst7nmg7ccr', 'Content-Type': 'application/json' } diff --git a/package-lock.json b/package-lock.json index 098fea0d..931e3178 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "5.2.2", + "@hookform/resolvers": "4.1.3", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", @@ -2232,14 +2232,15 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", - "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", + "integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==", + "license": "MIT", "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { - "react-hook-form": "^7.55.0" + "react-hook-form": "^7.0.0" } }, "node_modules/@humanfs/core": { diff --git a/package.json b/package.json index 342e28a7..f2370e52 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ }, "dependencies": { "@asteasolutions/zod-to-openapi": "^7.3.4", - "@hookform/resolvers": "5.2.2", + "@hookform/resolvers": "4.1.3", "@node-rs/argon2": "^2.0.2", "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index c6ab6f40..6244fefa 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -138,8 +138,12 @@ export async function updateProxyResources( ? true : resourceData.ssl; let headers = ""; - if (resourceData.headers) { - headers = JSON.stringify(resourceData.headers); + for (const header of resourceData.headers || []) { + headers += `${header.name}: ${header.value},`; + } + // if there are headers, remove the trailing comma + if (headers.endsWith(",")) { + headers = headers.slice(0, -1); } if (existingResource) { @@ -165,7 +169,7 @@ export async function updateProxyResources( .update(resources) .set({ name: resourceData.name || "Unnamed Resource", - protocol: protocol || "tcp", + protocol: protocol || "http", http: http, proxyPort: http ? null : resourceData["proxy-port"], fullDomain: http ? resourceData["full-domain"] : null, @@ -457,7 +461,7 @@ export async function updateProxyResources( orgId, niceId: resourceNiceId, name: resourceData.name || "Unnamed Resource", - protocol: protocol || "tcp", + protocol: resourceData.protocol || "http", http: http, proxyPort: http ? null : resourceData["proxy-port"], fullDomain: http ? resourceData["full-domain"] : null, diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 6c13963a..506c1c8d 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.10.2"; +export const APP_VERSION = "1.10.1"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/routers/idp/oidcAutoProvision.ts b/server/routers/idp/oidcAutoProvision.ts index 3571b59d..34cfcc8a 100644 --- a/server/routers/idp/oidcAutoProvision.ts +++ b/server/routers/idp/oidcAutoProvision.ts @@ -158,13 +158,8 @@ export async function oidcAutoProvision({ .from(userOrgs) .where(eq(userOrgs.userId, userId)); - // Filter to only auto-provisioned orgs for CRUD operations - const autoProvisionedOrgs = currentUserOrgs.filter( - (org) => org.autoProvisioned === true - ); - // Delete orgs that are no longer valid - const orgsToDelete = autoProvisionedOrgs + const orgsToDelete = currentUserOrgs .filter( (currentOrg) => !userOrgInfo.some( @@ -200,9 +195,7 @@ export async function oidcAutoProvision({ orgsToAdd.map((org) => ({ userId: userId!, orgId: org.orgId, - roleId: org.roleId, - autoProvisioned: true, - dateCreated: new Date().toISOString() + roleId: org.roleId })) ); } diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 803c3e27..91a0ac3f 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -15,7 +15,7 @@ export async function addTargets( }:${target.port}`; }); - await sendToClient(newtId, { + sendToClient(newtId, { type: `newt/${protocol}/add`, data: { targets: payloadTargets diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index a4311423..806a5a58 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -319,6 +319,26 @@ async function createRawResource( const { name, http, protocol, proxyPort } = parsedBody.data; + // if http is false check to see if there is already a resource with the same port and protocol + const existingResource = await db + .select() + .from(resources) + .where( + and( + eq(resources.protocol, protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); + + if (existingResource.length > 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + let resource: Resource | undefined; const niceId = await getUniqueResourceName(orgId); diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 0fdcdd0c..d2aebedd 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -42,9 +42,7 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) { } } -export type GetResourceResponse = Omit>>, 'headers'> & { - headers: { name: string; value: string }[] | null; -}; +export type GetResourceResponse = NonNullable>>; registry.registerPath({ method: "get", @@ -101,10 +99,7 @@ export async function getResource( } return response(res, { - data: { - ...resource, - headers: resource.headers ? JSON.parse(resource.headers) : resource.headers - }, + data: resource, success: true, error: false, message: "Resource retrieved successfully", diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index e70b9496..7c0f9c63 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -47,7 +47,7 @@ const updateHttpResourceBodySchema = z tlsServerName: z.string().nullable().optional(), setHostHeader: z.string().nullable().optional(), skipToIdpId: z.number().int().positive().nullable().optional(), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(), + headers: z.string().nullable().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -85,6 +85,18 @@ const updateHttpResourceBodySchema = z message: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } + ) + .refine( + (data) => { + if (data.headers) { + return validateHeaders(data.headers); + } + return true; + }, + { + message: + "Invalid headers format. Use comma-separated format: 'Header-Name: value, Another-Header: another-value'. Header values cannot contain colons." + } ); export type UpdateResourceResponse = Resource; @@ -235,7 +247,7 @@ async function updateHttpResource( // Validate domain and construct full domain const domainResult = await validateAndConstructDomain(domainId, resource.orgId, updateData.subdomain); - + if (!domainResult.success) { return next( createHttpError( @@ -280,14 +292,9 @@ async function updateHttpResource( updateData.subdomain = finalSubdomain; } - let headers = null; - if (updateData.headers) { - headers = JSON.stringify(updateData.headers); - } - const updatedResource = await db .update(resources) - .set({ ...updateData, headers }) + .set({ ...updateData }) .where(eq(resources.resourceId, resource.resourceId)) .returning(); @@ -335,6 +342,31 @@ async function updateRawResource( const updateData = parsedBody.data; + if (updateData.proxyPort) { + const proxyPort = updateData.proxyPort; + const existingResource = await db + .select() + .from(resources) + .where( + and( + eq(resources.protocol, resource.protocol), + eq(resources.proxyPort, proxyPort!) + ) + ); + + if ( + existingResource.length > 0 && + existingResource[0].resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource with that protocol and port already exists" + ) + ); + } + } + const updatedResource = await db .update(resources) .set(updateData) diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 5101de84..a1a2a7a3 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -306,25 +306,17 @@ export async function getTraefikConfig( ...additionalMiddlewares ]; - if (resource.headers || resource.setHostHeader) { + if (resource.headers && resource.headers.length > 0) { // if there are headers, parse them into an object const headersObj: { [key: string]: string } = {}; - if (resource.headers) { - let headersArr: { name: string; value: string }[] = []; - try { - headersArr = JSON.parse(resource.headers) as { - name: string; - value: string; - }[]; - } catch (e) { - logger.warn( - `Failed to parse headers for resource ${resource.resourceId}: ${e}` - ); + const headersArr = resource.headers.split(","); + for (const header of headersArr) { + const [key, value] = header + .split(":") + .map((s: string) => s.trim()); + if (key && value) { + headersObj[key] = value; } - - headersArr.forEach((header) => { - headersObj[header.name] = header.value; - }); } if (resource.setHostHeader) { diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 04779f30..c5950e1d 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -10,7 +10,6 @@ import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; import m4 from "./scriptsPg/1.9.0"; import m5 from "./scriptsPg/1.10.0"; -import m6 from "./scriptsPg/1.10.2"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -22,7 +21,6 @@ const migrations = [ { version: "1.8.0", run: m3 }, { version: "1.9.0", run: m4 }, { version: "1.10.0", run: m5 }, - { version: "1.10.2", run: m6 }, // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 654c2716..b8fa64f0 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -28,7 +28,6 @@ import m23 from "./scriptsSqlite/1.8.0"; import m24 from "./scriptsSqlite/1.9.0"; import m25 from "./scriptsSqlite/1.10.0"; import m26 from "./scriptsSqlite/1.10.1"; -import m27 from "./scriptsSqlite/1.10.2"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -56,7 +55,6 @@ const migrations = [ { version: "1.9.0", run: m24 }, { version: "1.10.0", run: m25 }, { version: "1.10.1", run: m26 }, - { version: "1.10.2", run: m27 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.10.2.ts b/server/setup/scriptsPg/1.10.2.ts deleted file mode 100644 index e59901a5..00000000 --- a/server/setup/scriptsPg/1.10.2.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { db } from "@server/db/pg/driver"; -import { sql } from "drizzle-orm"; -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; - -const version = "1.10.2"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - try { - const resources = await db.execute(sql` - SELECT * FROM "resources" - `); - - await db.execute(sql`BEGIN`); - - for (const resource of resources.rows) { - const headers = resource.headers as string | null; - if (headers && headers !== "") { - // lets convert it to json - // fist split at commas - const headersArray = headers - .split(",") - .map((header: string) => { - const [name, ...valueParts] = header.split(":"); - const value = valueParts.join(":").trim(); - return { name: name.trim(), value }; - }); - - await db.execute(sql` - UPDATE "resources" SET "headers" = ${JSON.stringify(headersArray)} WHERE "resourceId" = ${resource.resourceId} - `); - - console.log( - `Updated resource ${resource.resourceId} headers to JSON format` - ); - } - } - - await db.execute(sql`COMMIT`); - console.log(`Migrated database`); - } catch (e) { - await db.execute(sql`ROLLBACK`); - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/server/setup/scriptsSqlite/1.10.2.ts b/server/setup/scriptsSqlite/1.10.2.ts deleted file mode 100644 index 7978e262..00000000 --- a/server/setup/scriptsSqlite/1.10.2.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { APP_PATH } from "@server/lib/consts"; -import Database from "better-sqlite3"; -import path from "path"; - -const version = "1.10.2"; - -export default async function migration() { - console.log(`Running setup script ${version}...`); - - const location = path.join(APP_PATH, "db", "db.sqlite"); - const db = new Database(location); - - const resources = db.prepare("SELECT * FROM resources").all() as Array<{ - resourceId: number; - headers: string | null; - }>; - - try { - db.pragma("foreign_keys = OFF"); - - db.transaction(() => { - for (const resource of resources) { - const headers = resource.headers; - if (headers && headers !== "") { - // lets convert it to json - // fist split at commas - const headersArray = headers - .split(",") - .map((header: string) => { - const [name, ...valueParts] = header.split(":"); - const value = valueParts.join(":").trim(); - return { name: name.trim(), value }; - }); - - db.prepare( - ` - UPDATE "resources" SET "headers" = ? WHERE "resourceId" = ?` - ).run(JSON.stringify(headersArray), resource.resourceId); - - console.log( - `Updated resource ${resource.resourceId} headers to JSON format` - ); - } - } - })(); - - db.pragma("foreign_keys = ON"); - - console.log(`Migrated database`); - } catch (e) { - console.log("Failed to migrate db:", e); - throw e; - } -} diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index c92f2638..fdce89eb 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -68,7 +68,7 @@ export default function AccessControlsPage() { autoProvisioned: z.boolean() }); - const form = useForm({ + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { username: user.username!, diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx index 2df8413f..47bff1e1 100644 --- a/src/app/[orgId]/settings/access/users/create/page.tsx +++ b/src/app/[orgId]/settings/access/users/create/page.tsx @@ -161,7 +161,7 @@ export default function Page() { { hours: 168, name: t("day", { count: 7 }) } ]; - const internalForm = useForm({ + const internalForm = useForm>({ resolver: zodResolver(internalFormSchema), defaultValues: { email: "", @@ -170,7 +170,7 @@ export default function Page() { } }); - const googleAzureForm = useForm({ + const googleAzureForm = useForm>({ resolver: zodResolver(googleAzureFormSchema), defaultValues: { email: "", @@ -179,7 +179,7 @@ export default function Page() { } }); - const genericOidcForm = useForm({ + const genericOidcForm = useForm>({ resolver: zodResolver(genericOidcFormSchema), defaultValues: { username: "", diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx index b62c2628..57822a26 100644 --- a/src/app/[orgId]/settings/api-keys/create/page.tsx +++ b/src/app/[orgId]/settings/api-keys/create/page.tsx @@ -91,14 +91,14 @@ export default function Page() { type CopiedFormValues = z.infer; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createFormSchema), defaultValues: { name: "" } }); - const copiedForm = useForm({ + const copiedForm = useForm({ resolver: zodResolver(copiedFormSchema), defaultValues: { copied: true diff --git a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx index 55d7c0d3..27d708a4 100644 --- a/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/[clientId]/general/page.tsx @@ -58,7 +58,7 @@ export default function GeneralPage() { const [clientSites, setClientSites] = useState([]); const [activeSitesTagIndex, setActiveSitesTagIndex] = useState(null); - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: client?.name, diff --git a/src/app/[orgId]/settings/clients/create/page.tsx b/src/app/[orgId]/settings/clients/create/page.tsx index 8155a2d6..ac2a1c66 100644 --- a/src/app/[orgId]/settings/clients/create/page.tsx +++ b/src/app/[orgId]/settings/clients/create/page.tsx @@ -265,7 +265,7 @@ export default function Page() { } }; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createClientFormSchema), defaultValues: { name: "", diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index c4bb3ccc..0eba0a3d 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -59,7 +59,7 @@ export default function GeneralPage() { const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: org?.org.name, diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index d53cb0c0..4705550e 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -138,12 +138,12 @@ export default function ResourceAuthenticationPage() { const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const usersRolesForm = useForm({ + const usersRolesForm = useForm>({ resolver: zodResolver(UsersRolesFormSchema), defaultValues: { roles: [], users: [] } }); - const whitelistForm = useForm({ + const whitelistForm = useForm>({ resolver: zodResolver(whitelistSchema), defaultValues: { emails: [] } }); diff --git a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx index 21d601ed..0f201a1a 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx @@ -119,7 +119,7 @@ export default function GeneralForm() { type GeneralFormValues = z.infer; - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { enabled: resource.enabled, diff --git a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx index 4c2eedf5..ba71a765 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/proxy/page.tsx @@ -227,7 +227,7 @@ export default function ReverseProxyTargets(props: { message: t("proxyErrorInvalidHeader") } ), - headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable() + headers: z.string().optional() }); const tlsSettingsSchema = z.object({ @@ -260,7 +260,7 @@ export default function ReverseProxyTargets(props: { port: "" as any as number, path: null, pathMatchType: null - } + } as z.infer }); const watchedIp = addTargetForm.watch("ip"); @@ -274,7 +274,7 @@ export default function ReverseProxyTargets(props: { } }; - const tlsSettingsForm = useForm({ + const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), defaultValues: { ssl: resource.ssl, @@ -282,15 +282,15 @@ export default function ReverseProxyTargets(props: { } }); - const proxySettingsForm = useForm({ + const proxySettingsForm = useForm({ resolver: zodResolver(proxySettingsSchema), defaultValues: { setHostHeader: resource.setHostHeader || "", - headers: resource.headers + headers: resource.headers || "" } }); - const targetsSettingsForm = useForm({ + const targetsSettingsForm = useForm({ resolver: zodResolver(targetsSettingsSchema), defaultValues: { stickySession: resource.stickySession @@ -1479,7 +1479,7 @@ export default function ReverseProxyTargets(props: { { field.onChange( diff --git a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx index 284573b2..8b5e4709 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/rules/page.tsx @@ -114,7 +114,7 @@ export default function ResourceRules(props: { CIDR: t('ipAddressRange') } as const; - const addRuleForm = useForm({ + const addRuleForm = useForm>({ resolver: zodResolver(addRuleSchema), defaultValues: { action: "ACCEPT", diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index f551e418..71628ce7 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -211,7 +211,7 @@ export default function Page() { ]) ]; - const baseForm = useForm({ + const baseForm = useForm({ resolver: zodResolver(baseResourceFormSchema), defaultValues: { name: "", @@ -219,12 +219,12 @@ export default function Page() { } }); - const httpForm = useForm({ + const httpForm = useForm({ resolver: zodResolver(httpResourceFormSchema), defaultValues: {} }); - const tcpUdpForm = useForm({ + const tcpUdpForm = useForm({ resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", @@ -241,7 +241,7 @@ export default function Page() { port: "" as any as number, path: null, pathMatchType: null - } + } as z.infer }); const watchedIp = addTargetForm.watch("ip"); diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index 432d4bd3..8bd8dc4b 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -64,7 +64,7 @@ export default function GeneralPage() { const router = useRouter(); const t = useTranslations(); - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: site?.name, diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index ad5438f7..2e5d4e45 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -425,7 +425,7 @@ WantedBy=default.target` } }; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createSiteFormSchema), defaultValues: { name: "", diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx index 65f8e46a..b5a61306 100644 --- a/src/app/admin/api-keys/create/page.tsx +++ b/src/app/admin/api-keys/create/page.tsx @@ -89,14 +89,14 @@ export default function Page() { type CopiedFormValues = z.infer; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createFormSchema), defaultValues: { name: "" } }); - const copiedForm = useForm({ + const copiedForm = useForm({ resolver: zodResolver(copiedFormSchema), defaultValues: { copied: true diff --git a/src/app/admin/idp/[idpId]/general/page.tsx b/src/app/admin/idp/[idpId]/general/page.tsx index 1eca54e7..8aa4f084 100644 --- a/src/app/admin/idp/[idpId]/general/page.tsx +++ b/src/app/admin/idp/[idpId]/general/page.tsx @@ -71,7 +71,7 @@ export default function GeneralPage() { type GeneralFormValues = z.infer; - const form = useForm({ + const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { name: "", diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index 8c895b8b..01b186bf 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -102,7 +102,7 @@ export default function PoliciesPage() { type PolicyFormValues = z.infer; type DefaultMappingsValues = z.infer; - const form = useForm({ + const form = useForm({ resolver: zodResolver(policyFormSchema), defaultValues: { orgId: "", @@ -111,7 +111,7 @@ export default function PoliciesPage() { } }); - const defaultMappingsForm = useForm({ + const defaultMappingsForm = useForm({ resolver: zodResolver(defaultMappingsSchema), defaultValues: { defaultRoleMapping: "", diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 27c26ed6..3a645d27 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -76,7 +76,7 @@ export default function Page() { } ]; - const form = useForm({ + const form = useForm({ resolver: zodResolver(createIdpFormSchema), defaultValues: { name: "", diff --git a/src/app/auth/initial-setup/page.tsx b/src/app/auth/initial-setup/page.tsx index 4a443896..e1dd3f06 100644 --- a/src/app/auth/initial-setup/page.tsx +++ b/src/app/auth/initial-setup/page.tsx @@ -51,7 +51,7 @@ export default function InitialSetupPage() { const [error, setError] = useState(null); const [checking, setChecking] = useState(true); - const form = useForm({ + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { setupToken: "", diff --git a/src/app/auth/reset-password/ResetPasswordForm.tsx b/src/app/auth/reset-password/ResetPasswordForm.tsx index 14199493..3d456bd9 100644 --- a/src/app/auth/reset-password/ResetPasswordForm.tsx +++ b/src/app/auth/reset-password/ResetPasswordForm.tsx @@ -102,7 +102,7 @@ export default function ResetPasswordForm({ code: z.string().length(6, { message: t('pincodeInvalid') }) }); - const form = useForm({ + const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: emailParam || "", @@ -112,14 +112,14 @@ export default function ResetPasswordForm({ } }); - const mfaForm = useForm({ + const mfaForm = useForm>({ resolver: zodResolver(mfaSchema), defaultValues: { code: "" } }); - const requestForm = useForm({ + const requestForm = useForm>({ resolver: zodResolver(requestSchema), defaultValues: { email: emailParam || "" diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 65ffc786..42c64b16 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -50,7 +50,7 @@ export default function StepperForm() { subnet: z.string().min(1, { message: t("subnetRequired") }) }); - const orgForm = useForm({ + const orgForm = useForm>({ resolver: zodResolver(orgSchema), defaultValues: { orgName: "", diff --git a/src/components/HeadersInput.tsx b/src/components/HeadersInput.tsx index 4a93ddc8..35f3e587 100644 --- a/src/components/HeadersInput.tsx +++ b/src/components/HeadersInput.tsx @@ -1,118 +1,66 @@ "use client"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState } from "react"; import { Textarea } from "@/components/ui/textarea"; - interface HeadersInputProps { - value?: { name: string, value: string }[] | null; - onChange: (value: { name: string, value: string }[] | null) => void; + value?: string; + onChange: (value: string) => void; placeholder?: string; rows?: number; className?: string; } -export function HeadersInput({ - value = [], - onChange, +export function HeadersInput({ + value = "", + onChange, placeholder = `X-Example-Header: example-value X-Another-Header: another-value`, rows = 4, className }: HeadersInputProps) { const [internalValue, setInternalValue] = useState(""); - const textareaRef = useRef(null); - const isUserEditingRef = useRef(false); - // Convert header objects array to newline-separated string for display - const convertToNewlineSeparated = (headers: { name: string, value: string }[] | null): string => { - if (!headers || headers.length === 0) return ""; - - return headers - .map(header => `${header.name}: ${header.value}`) + // Convert comma-separated to newline-separated for display + const convertToNewlineSeparated = (commaSeparated: string): string => { + if (!commaSeparated || commaSeparated.trim() === "") return ""; + + return commaSeparated + .split(',') + .map(header => header.trim()) + .filter(header => header.length > 0) .join('\n'); }; - // Convert newline-separated string to header objects array - const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => { - if (!newlineSeparated || newlineSeparated.trim() === "") return []; - + // Convert newline-separated to comma-separated for output + const convertToCommaSeparated = (newlineSeparated: string): string => { + if (!newlineSeparated || newlineSeparated.trim() === "") return ""; + return newlineSeparated .split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0 && line.includes(':')) - .map(line => { - const colonIndex = line.indexOf(':'); - const name = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim(); - - // Ensure header name conforms to HTTP header requirements - // Header names should be case-insensitive, contain only ASCII letters, digits, and hyphens - const normalizedName = name.replace(/[^a-zA-Z0-9\-]/g, '').toLowerCase(); - - return { name: normalizedName, value }; - }) - .filter(header => header.name.length > 0); // Filter out headers with invalid names + .map(header => header.trim()) + .filter(header => header.length > 0) + .join(', '); }; // Update internal value when external value changes - // But only if the user is not currently editing (textarea not focused) useEffect(() => { - if (!isUserEditingRef.current) { - setInternalValue(convertToNewlineSeparated(value)); - } + setInternalValue(convertToNewlineSeparated(value)); }, [value]); const handleChange = (e: React.ChangeEvent) => { const newValue = e.target.value; setInternalValue(newValue); - - // Mark that user is actively editing - isUserEditingRef.current = true; - - // Only update parent if the input is in a valid state - // Valid states: empty/whitespace only, or contains properly formatted headers - - if (newValue.trim() === "") { - // Empty input is valid - represents no headers - onChange([]); - } else { - // Check if all non-empty lines are properly formatted (contain ':') - const lines = newValue.split('\n'); - const nonEmptyLines = lines - .map(line => line.trim()) - .filter(line => line.length > 0); - - // If there are no non-empty lines, or all non-empty lines contain ':', it's valid - const isValid = nonEmptyLines.length === 0 || nonEmptyLines.every(line => line.includes(':')); - - if (isValid) { - // Safe to convert and update parent - const headersArray = convertToHeadersArray(newValue); - onChange(headersArray); - } - // If not valid, don't call onChange - let user continue typing - } - }; - - const handleFocus = () => { - isUserEditingRef.current = true; - }; - - const handleBlur = () => { - // Small delay to allow any final change events to process - setTimeout(() => { - isUserEditingRef.current = false; - }, 100); + + // Convert back to comma-separated format for the parent + const commaSeparatedValue = convertToCommaSeparated(newValue); + onChange(commaSeparatedValue); }; return (