Compare commits

...

29 Commits

Author SHA1 Message Date
Owen
d10fdac670 Merge branch 'dev' 2025-09-23 14:36:46 -07:00
Owen
21f0cd6e3f Fix #1527 2025-09-23 09:30:18 -04:00
Owen Schwartz
b2ee8ef7de Merge pull request #1525 from Lokowitz/Resolver
Fix upgrade @hookform/resolvers from 4.1.3 to 5.2.2
2025-09-23 09:19:08 -04:00
Marvin
1e066cbabd fix components 2025-09-22 20:22:31 +00:00
Marvin
4cc38d44e0 Merge branch 'Resolver' of https://github.com/Lokowitz/pangolin into Resolver 2025-09-22 20:10:21 +00:00
Marvin
dcf7393259 update resolver 2025-09-22 20:06:55 +00:00
Marvin
bab070b09c page.tsx aktualisieren 2025-09-22 17:34:52 +02:00
Marvin
2bd4ad5770 page.tsx aktualisieren 2025-09-22 17:20:32 +02:00
Marvin
61ecebf911 bbbv 2025-09-22 15:13:29 +00:00
Marvin
33c8663a5b package.json aktualisieren 2025-09-22 17:04:30 +02:00
Owen
1f9f3fdede Merge branch 'dev' 2025-09-21 22:25:09 -04:00
Owen
a778109214 Fix using wrong protocol when creating resource 2025-09-21 22:25:05 -04:00
Owen
cb7fa9375b Make sure to process headers correctly in blueprint 2025-09-21 22:25:05 -04:00
Owen
515ecb09e7 Update url and remove example token 2025-09-21 22:25:04 -04:00
Owen
a12a620697 Fix using wrong protocol when creating resource 2025-09-21 22:24:54 -04:00
Owen
0c3b2bc2f5 Make sure to process headers correctly in blueprint 2025-09-21 22:24:53 -04:00
Owen
78ba27dc63 Update url and remove example token 2025-09-21 22:24:53 -04:00
Owen Schwartz
dc20b863ed Merge pull request #1512 from fosrl/dev
1.10.2
2025-09-21 22:24:29 -04:00
Owen Schwartz
c9a211d5cf Merge pull request #1505 from fosrl/crowdin_dev
New Crowdin updates
2025-09-21 21:01:25 -04:00
Owen
95f94cffd2 Fix lint 2025-09-21 20:50:01 -04:00
Owen
0da95cbdb8 Version correctly 2025-09-21 20:48:13 -04:00
Owen
dadd1e3101 Add migration to manager 2025-09-21 16:44:08 -04:00
Owen
d523ae3ffa Fix input overwriting value 2025-09-21 16:39:40 -04:00
Owen
9a41cac6e1 Remove port checks 2025-09-21 16:16:41 -04:00
Owen
5d3c5ab7cc Store headers as json 2025-09-21 15:49:50 -04:00
Owen
e94ded920b Fix #1501 2025-09-21 11:42:51 -04:00
Owen Schwartz
c882fbd59a New translations en-us.json (German) 2025-09-20 09:51:23 -04:00
Owen
46b50a042e Merge branch 'dev' of github.com:fosrl/pangolin into dev 2025-09-18 21:52:56 -04:00
Owen
fda9e95786 Add header for host all the time 2025-09-18 21:52:02 -04:00
49 changed files with 287 additions and 178 deletions

View File

@@ -8,10 +8,10 @@ import base64
YAML_FILE_PATH = 'blueprint.yaml'
# The API endpoint and headers from the curl request
API_URL = 'http://localhost:3004/v1/org/test/blueprint'
API_URL = 'http://api.pangolin.fossorial.io/v1/org/test/blueprint'
HEADERS = {
'accept': '*/*',
'Authorization': 'Bearer v7ix7xha1bmq2on.tzsden374mtmkeczm3tx44uzxsljnrst7nmg7ccr',
'Authorization': 'Bearer <your_token_here>',
'Content-Type': 'application/json'
}

View File

@@ -454,7 +454,7 @@
"accessRoleErrorAddDescription": "Beim Hinzufügen des Benutzers zur Rolle ist ein Fehler aufgetreten.",
"userSaved": "Benutzer gespeichert",
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
"autoProvisioned": "Automatisch vorgesehen",
"autoProvisioned": "Automatisch bereitgestellt",
"autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter",
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
"accessControlsSubmit": "Zugriffskontrollen speichern",

11
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "SEE LICENSE IN LICENSE AND README.md",
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4",
"@hookform/resolvers": "4.1.3",
"@hookform/resolvers": "5.2.2",
"@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
@@ -2233,15 +2233,14 @@
"license": "MIT"
},
"node_modules/@hookform/resolvers": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz",
"integrity": "sha512-Jsv6UOWYTrEFJ/01ZrnwVXs7KDvP8XIo115i++5PWvNkNvkrsTfGiLS6w+eJ57CYtUtDQalUWovCZDHFJ8u1VQ==",
"license": "MIT",
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"react-hook-form": "^7.0.0"
"react-hook-form": "^7.55.0"
}
},
"node_modules/@humanfs/core": {

View File

@@ -27,7 +27,7 @@
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4",
"@hookform/resolvers": "4.1.3",
"@hookform/resolvers": "5.2.2",
"@node-rs/argon2": "^2.0.2",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",

View File

@@ -138,12 +138,8 @@ export async function updateProxyResources(
? true
: resourceData.ssl;
let 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 (resourceData.headers) {
headers = JSON.stringify(resourceData.headers);
}
if (existingResource) {
@@ -169,7 +165,7 @@ export async function updateProxyResources(
.update(resources)
.set({
name: resourceData.name || "Unnamed Resource",
protocol: protocol || "http",
protocol: protocol || "tcp",
http: http,
proxyPort: http ? null : resourceData["proxy-port"],
fullDomain: http ? resourceData["full-domain"] : null,
@@ -461,7 +457,7 @@ export async function updateProxyResources(
orgId,
niceId: resourceNiceId,
name: resourceData.name || "Unnamed Resource",
protocol: resourceData.protocol || "http",
protocol: protocol || "tcp",
http: http,
proxyPort: http ? null : resourceData["proxy-port"],
fullDomain: http ? resourceData["full-domain"] : null,

View File

@@ -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.1";
export const APP_VERSION = "1.10.2";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);

View File

@@ -15,7 +15,7 @@ export async function addTargets(
}:${target.port}`;
});
sendToClient(newtId, {
await sendToClient(newtId, {
type: `newt/${protocol}/add`,
data: {
targets: payloadTargets

View File

@@ -319,26 +319,6 @@ 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);

View File

@@ -42,7 +42,9 @@ async function query(resourceId?: number, niceId?: string, orgId?: string) {
}
}
export type GetResourceResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
export type GetResourceResponse = Omit<NonNullable<Awaited<ReturnType<typeof query>>>, 'headers'> & {
headers: { name: string; value: string }[] | null;
};
registry.registerPath({
method: "get",
@@ -99,7 +101,10 @@ export async function getResource(
}
return response<GetResourceResponse>(res, {
data: resource,
data: {
...resource,
headers: resource.headers ? JSON.parse(resource.headers) : resource.headers
},
success: true,
error: false,
message: "Resource retrieved successfully",

View File

@@ -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.string().nullable().optional()
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable().optional(),
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@@ -85,18 +85,6 @@ 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;
@@ -292,9 +280,14 @@ 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 })
.set({ ...updateData, headers })
.where(eq(resources.resourceId, resource.resourceId))
.returning();
@@ -342,31 +335,6 @@ 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)

View File

@@ -306,17 +306,25 @@ export async function getTraefikConfig(
...additionalMiddlewares
];
if (resource.headers && resource.headers.length > 0) {
if (resource.headers || resource.setHostHeader) {
// if there are headers, parse them into an object
const headersObj: { [key: string]: string } = {};
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;
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}`
);
}
headersArr.forEach((header) => {
headersObj[header.name] = header.value;
});
}
if (resource.setHostHeader) {

View File

@@ -10,6 +10,7 @@ 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
@@ -21,6 +22,7 @@ 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;

View File

@@ -28,6 +28,7 @@ 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
@@ -55,6 +56,7 @@ 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;

View File

@@ -0,0 +1,47 @@
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;
}
}

View File

@@ -66,4 +66,4 @@ DROP TABLE "targets_old";`);
console.log("Failed to migrate db:", e);
throw e;
}
}
}

View File

@@ -0,0 +1,54 @@
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;
}
}

View File

@@ -63,7 +63,7 @@ export default function AccessControlsPage() {
autoProvisioned: z.boolean()
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
username: user.username!,

View File

@@ -161,7 +161,7 @@ export default function Page() {
{ hours: 168, name: t("day", { count: 7 }) }
];
const internalForm = useForm<z.infer<typeof internalFormSchema>>({
const internalForm = useForm({
resolver: zodResolver(internalFormSchema),
defaultValues: {
email: "",
@@ -170,7 +170,7 @@ export default function Page() {
}
});
const googleAzureForm = useForm<z.infer<typeof googleAzureFormSchema>>({
const googleAzureForm = useForm({
resolver: zodResolver(googleAzureFormSchema),
defaultValues: {
email: "",
@@ -179,7 +179,7 @@ export default function Page() {
}
});
const genericOidcForm = useForm<z.infer<typeof genericOidcFormSchema>>({
const genericOidcForm = useForm({
resolver: zodResolver(genericOidcFormSchema),
defaultValues: {
username: "",

View File

@@ -91,14 +91,14 @@ export default function Page() {
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
const form = useForm<CreateFormValues>({
const form = useForm({
resolver: zodResolver(createFormSchema),
defaultValues: {
name: ""
}
});
const copiedForm = useForm<CopiedFormValues>({
const copiedForm = useForm({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: true

View File

@@ -58,7 +58,7 @@ export default function GeneralPage() {
const [clientSites, setClientSites] = useState<Tag[]>([]);
const [activeSitesTagIndex, setActiveSitesTagIndex] = useState<number | null>(null);
const form = useForm<GeneralFormValues>({
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: client?.name,

View File

@@ -265,7 +265,7 @@ export default function Page() {
}
};
const form = useForm<CreateClientFormValues>({
const form = useForm({
resolver: zodResolver(createClientFormSchema),
defaultValues: {
name: "",

View File

@@ -59,7 +59,7 @@ export default function GeneralPage() {
const [loadingDelete, setLoadingDelete] = useState(false);
const [loadingSave, setLoadingSave] = useState(false);
const form = useForm<GeneralFormValues>({
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: org?.org.name,

View File

@@ -138,12 +138,12 @@ export default function ResourceAuthenticationPage() {
const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false);
const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false);
const usersRolesForm = useForm<z.infer<typeof UsersRolesFormSchema>>({
const usersRolesForm = useForm({
resolver: zodResolver(UsersRolesFormSchema),
defaultValues: { roles: [], users: [] }
});
const whitelistForm = useForm<z.infer<typeof whitelistSchema>>({
const whitelistForm = useForm({
resolver: zodResolver(whitelistSchema),
defaultValues: { emails: [] }
});

View File

@@ -119,7 +119,7 @@ export default function GeneralForm() {
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm<GeneralFormValues>({
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
enabled: resource.enabled,

View File

@@ -227,7 +227,7 @@ export default function ReverseProxyTargets(props: {
message: t("proxyErrorInvalidHeader")
}
),
headers: z.string().optional()
headers: z.array(z.object({ name: z.string(), value: z.string() })).nullable()
});
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<typeof addTargetSchema>
}
});
const watchedIp = addTargetForm.watch("ip");
@@ -274,7 +274,7 @@ export default function ReverseProxyTargets(props: {
}
};
const tlsSettingsForm = useForm<TlsSettingsValues>({
const tlsSettingsForm = useForm({
resolver: zodResolver(tlsSettingsSchema),
defaultValues: {
ssl: resource.ssl,
@@ -282,15 +282,15 @@ export default function ReverseProxyTargets(props: {
}
});
const proxySettingsForm = useForm<ProxySettingsValues>({
const proxySettingsForm = useForm({
resolver: zodResolver(proxySettingsSchema),
defaultValues: {
setHostHeader: resource.setHostHeader || "",
headers: resource.headers || ""
headers: resource.headers
}
});
const targetsSettingsForm = useForm<TargetsSettingsValues>({
const targetsSettingsForm = useForm({
resolver: zodResolver(targetsSettingsSchema),
defaultValues: {
stickySession: resource.stickySession
@@ -1479,7 +1479,7 @@ export default function ReverseProxyTargets(props: {
<FormControl>
<HeadersInput
value={
field.value || ""
field.value
}
onChange={(value) => {
field.onChange(

View File

@@ -114,7 +114,7 @@ export default function ResourceRules(props: {
CIDR: t('ipAddressRange')
} as const;
const addRuleForm = useForm<z.infer<typeof addRuleSchema>>({
const addRuleForm = useForm({
resolver: zodResolver(addRuleSchema),
defaultValues: {
action: "ACCEPT",

View File

@@ -211,7 +211,7 @@ export default function Page() {
])
];
const baseForm = useForm<BaseResourceFormValues>({
const baseForm = useForm({
resolver: zodResolver(baseResourceFormSchema),
defaultValues: {
name: "",
@@ -219,12 +219,12 @@ export default function Page() {
}
});
const httpForm = useForm<HttpResourceFormValues>({
const httpForm = useForm({
resolver: zodResolver(httpResourceFormSchema),
defaultValues: {}
});
const tcpUdpForm = useForm<TcpUdpResourceFormValues>({
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<typeof addTargetSchema>
}
});
const watchedIp = addTargetForm.watch("ip");

View File

@@ -64,7 +64,7 @@ export default function GeneralPage() {
const router = useRouter();
const t = useTranslations();
const form = useForm<GeneralFormValues>({
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: site?.name,

View File

@@ -425,7 +425,7 @@ WantedBy=default.target`
}
};
const form = useForm<CreateSiteFormValues>({
const form = useForm({
resolver: zodResolver(createSiteFormSchema),
defaultValues: {
name: "",

View File

@@ -89,14 +89,14 @@ export default function Page() {
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
const form = useForm<CreateFormValues>({
const form = useForm({
resolver: zodResolver(createFormSchema),
defaultValues: {
name: ""
}
});
const copiedForm = useForm<CopiedFormValues>({
const copiedForm = useForm({
resolver: zodResolver(copiedFormSchema),
defaultValues: {
copied: true

View File

@@ -74,7 +74,7 @@ export default function GeneralPage() {
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
const form = useForm<GeneralFormValues>({
const form = useForm({
resolver: zodResolver(GeneralFormSchema),
defaultValues: {
name: "",

View File

@@ -102,7 +102,7 @@ export default function PoliciesPage() {
type PolicyFormValues = z.infer<typeof policyFormSchema>;
type DefaultMappingsValues = z.infer<typeof defaultMappingsSchema>;
const form = useForm<PolicyFormValues>({
const form = useForm({
resolver: zodResolver(policyFormSchema),
defaultValues: {
orgId: "",
@@ -111,7 +111,7 @@ export default function PoliciesPage() {
}
});
const defaultMappingsForm = useForm<DefaultMappingsValues>({
const defaultMappingsForm = useForm({
resolver: zodResolver(defaultMappingsSchema),
defaultValues: {
defaultRoleMapping: "",

View File

@@ -79,7 +79,7 @@ export default function Page() {
}
];
const form = useForm<CreateIdpFormValues>({
const form = useForm({
resolver: zodResolver(createIdpFormSchema),
defaultValues: {
name: "",

View File

@@ -97,7 +97,7 @@ export default function LicensePage() {
})
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
licenseKey: "",

View File

@@ -51,7 +51,7 @@ export default function InitialSetupPage() {
const [error, setError] = useState<string | null>(null);
const [checking, setChecking] = useState(true);
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
setupToken: "",

View File

@@ -102,7 +102,7 @@ export default function ResetPasswordForm({
code: z.string().length(6, { message: t('pincodeInvalid') })
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: emailParam || "",
@@ -112,14 +112,14 @@ export default function ResetPasswordForm({
}
});
const mfaForm = useForm<z.infer<typeof mfaSchema>>({
const mfaForm = useForm({
resolver: zodResolver(mfaSchema),
defaultValues: {
code: ""
}
});
const requestForm = useForm<z.infer<typeof requestSchema>>({
const requestForm = useForm({
resolver: zodResolver(requestSchema),
defaultValues: {
email: emailParam || ""

View File

@@ -50,7 +50,7 @@ export default function StepperForm() {
subnet: z.string().min(1, { message: t("subnetRequired") })
});
const orgForm = useForm<z.infer<typeof orgSchema>>({
const orgForm = useForm({
resolver: zodResolver(orgSchema),
defaultValues: {
orgName: "",

View File

@@ -1,18 +1,19 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useRef } from "react";
import { Textarea } from "@/components/ui/textarea";
interface HeadersInputProps {
value?: string;
onChange: (value: string) => void;
value?: { name: string, value: string }[] | null;
onChange: (value: { name: string, value: string }[] | null) => void;
placeholder?: string;
rows?: number;
className?: string;
}
export function HeadersInput({
value = "",
value = [],
onChange,
placeholder = `X-Example-Header: example-value
X-Another-Header: another-value`,
@@ -20,47 +21,98 @@ X-Another-Header: another-value`,
className
}: HeadersInputProps) {
const [internalValue, setInternalValue] = useState("");
const textareaRef = useRef<HTMLTextAreaElement>(null);
const isUserEditingRef = useRef(false);
// Convert comma-separated to newline-separated for display
const convertToNewlineSeparated = (commaSeparated: string): string => {
if (!commaSeparated || commaSeparated.trim() === "") return "";
// 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 commaSeparated
.split(',')
.map(header => header.trim())
.filter(header => header.length > 0)
return headers
.map(header => `${header.name}: ${header.value}`)
.join('\n');
};
// Convert newline-separated to comma-separated for output
const convertToCommaSeparated = (newlineSeparated: string): string => {
if (!newlineSeparated || newlineSeparated.trim() === "") return "";
// Convert newline-separated string to header objects array
const convertToHeadersArray = (newlineSeparated: string): { name: string, value: string }[] | null => {
if (!newlineSeparated || newlineSeparated.trim() === "") return [];
return newlineSeparated
.split('\n')
.map(header => header.trim())
.filter(header => header.length > 0)
.join(', ');
.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
};
// Update internal value when external value changes
// But only if the user is not currently editing (textarea not focused)
useEffect(() => {
setInternalValue(convertToNewlineSeparated(value));
if (!isUserEditingRef.current) {
setInternalValue(convertToNewlineSeparated(value));
}
}, [value]);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value;
setInternalValue(newValue);
// Convert back to comma-separated format for the parent
const commaSeparatedValue = convertToCommaSeparated(newValue);
onChange(commaSeparatedValue);
// 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);
};
return (
<Textarea
ref={textareaRef}
value={internalValue}
onChange={handleChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
rows={rows}
className={className}

View File

@@ -84,7 +84,7 @@ export function IdpCreateWizard({ onSubmit, defaultValues, loading = false }: Id
}
];
const form = useForm<CreateIdpFormValues>({
const form = useForm({
resolver: zodResolver(createIdpFormSchema),
defaultValues: {
name: "",

View File

@@ -80,7 +80,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
code: z.string().length(6, { message: t("pincodeInvalid") })
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: "",
@@ -88,7 +88,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
}
});
const mfaForm = useForm<z.infer<typeof mfaSchema>>({
const mfaForm = useForm({
resolver: zodResolver(mfaSchema),
defaultValues: {
code: ""

View File

@@ -102,7 +102,7 @@ export default function ResetPasswordForm({
code: z.string().length(6, { message: t('pincodeInvalid') })
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: emailParam || "",
@@ -112,14 +112,14 @@ export default function ResetPasswordForm({
}
});
const mfaForm = useForm<z.infer<typeof mfaSchema>>({
const mfaForm = useForm({
resolver: zodResolver(mfaSchema),
defaultValues: {
code: ""
}
});
const requestForm = useForm<z.infer<typeof requestSchema>>({
const requestForm = useForm({
resolver: zodResolver(requestSchema),
defaultValues: {
email: emailParam || ""

View File

@@ -135,28 +135,28 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
const [activeTab, setActiveTab] = useState(getDefaultSelectedMethod());
const pinForm = useForm<z.infer<typeof pinSchema>>({
const pinForm = useForm({
resolver: zodResolver(pinSchema),
defaultValues: {
pin: ""
}
});
const passwordForm = useForm<z.infer<typeof passwordSchema>>({
const passwordForm = useForm({
resolver: zodResolver(passwordSchema),
defaultValues: {
password: ""
}
});
const requestOtpForm = useForm<z.infer<typeof requestOtpSchema>>({
const requestOtpForm = useForm({
resolver: zodResolver(requestOtpSchema),
defaultValues: {
email: ""
}
});
const submitOtpForm = useForm<z.infer<typeof submitOtpSchema>>({
const submitOtpForm = useForm({
resolver: zodResolver(submitOtpSchema),
defaultValues: {
email: "",

View File

@@ -119,7 +119,7 @@ export default function SecurityKeyForm({
code: z.string().optional()
});
const registerForm = useForm<RegisterFormValues>({
const registerForm = useForm({
resolver: zodResolver(registerSchema),
defaultValues: {
name: "",
@@ -128,7 +128,7 @@ export default function SecurityKeyForm({
}
});
const deleteForm = useForm<DeleteFormValues>({
const deleteForm = useForm({
resolver: zodResolver(deleteSchema),
defaultValues: {
password: "",

View File

@@ -39,10 +39,6 @@ const setPasswordFormSchema = z.object({
type SetPasswordFormValues = z.infer<typeof setPasswordFormSchema>;
const defaultValues: Partial<SetPasswordFormValues> = {
password: ""
};
type SetPasswordFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
@@ -61,9 +57,11 @@ export default function SetResourcePasswordForm({
const [loading, setLoading] = useState(false);
const form = useForm<SetPasswordFormValues>({
const form = useForm({
resolver: zodResolver(setPasswordFormSchema),
defaultValues
defaultValues: {
password: ""
}
});
useEffect(() => {

View File

@@ -44,10 +44,6 @@ const setPincodeFormSchema = z.object({
type SetPincodeFormValues = z.infer<typeof setPincodeFormSchema>;
const defaultValues: Partial<SetPincodeFormValues> = {
pincode: ""
};
type SetPincodeFormProps = {
open: boolean;
setOpen: (open: boolean) => void;
@@ -65,9 +61,11 @@ export default function SetResourcePincodeForm({
const api = createApiClient(useEnvContext());
const form = useForm<SetPincodeFormValues>({
const form = useForm({
resolver: zodResolver(setPincodeFormSchema),
defaultValues
defaultValues: {
pincode: ""
}
});
const t = useTranslations();

View File

@@ -117,7 +117,7 @@ export default function SignupForm({
const [passwordValue, setPasswordValue] = useState("");
const [confirmPasswordValue, setConfirmPasswordValue] = useState("");
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
email: emailParam || "",

View File

@@ -78,7 +78,7 @@ export default function SupporterStatus({ isCollapsed = false }: SupporterStatus
key: z.string().nonempty({ message: "Supporter key is required" })
});
const form = useForm<z.infer<typeof formSchema>>({
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
githubUsername: "",

View File

@@ -91,14 +91,14 @@ const TwoFactorSetupForm = forwardRef<
code: z.string().length(6, { message: t("pincodeInvalid") })
});
const enableForm = useForm<z.infer<typeof enableSchema>>({
const enableForm = useForm({
resolver: zodResolver(enableSchema),
defaultValues: {
password: initialPassword || ""
}
});
const confirmForm = useForm<z.infer<typeof confirmSchema>>({
const confirmForm = useForm({
resolver: zodResolver(confirmSchema),
defaultValues: {
code: ""

View File

@@ -80,7 +80,7 @@ export default function VerifyEmailForm({
})
});
const form = useForm<z.infer<typeof FormSchema>>({
const form = useForm({
resolver: zodResolver(FormSchema),
defaultValues: {
email: email,