From 850e9a734ad243ac968a0109d34a3ee89f09770f Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 6 Oct 2025 10:14:02 -0700 Subject: [PATCH] Adding HTTP Header Authentication --- server/auth/actions.ts | 2 +- server/db/pg/schema.ts | 9 + server/db/queries/verifySessionQueries.ts | 10 +- server/db/sqlite/schema.ts | 11 ++ server/routers/badger/verifySession.ts | 104 ++++++---- server/routers/external.ts | 7 + server/routers/integration.ts | 12 +- .../routers/resource/getResourceAuthInfo.ts | 28 ++- server/routers/resource/index.ts | 1 + .../routers/resource/setResourceHeaderAuth.ts | 101 ++++++++++ .../[niceId]/authentication/page.tsx | 86 +++++++- src/components/PermissionsSelectBox.tsx | 1 + src/components/SetResourceHeaderAuthForm.tsx | 186 ++++++++++++++++++ 13 files changed, 516 insertions(+), 42 deletions(-) create mode 100644 server/routers/resource/setResourceHeaderAuth.ts create mode 100644 src/components/SetResourceHeaderAuthForm.tsx diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 45d53eaa..668be0db 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -61,6 +61,7 @@ export enum ActionsEnum { getUser = "getUser", setResourcePassword = "setResourcePassword", setResourcePincode = "setResourcePincode", + setResourceHeaderAuth = "setResourceHeaderAuth", setResourceWhitelist = "setResourceWhitelist", getResourceWhitelist = "getResourceWhitelist", generateAccessToken = "generateAccessToken", @@ -194,7 +195,6 @@ export async function checkUserActionPermission( return roleActionPermission.length > 0; - return false; } catch (error) { console.error("Error checking user action permission:", error); throw createHttpError( diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 29c14560..959f99ec 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -380,6 +380,14 @@ export const resourcePassword = pgTable("resourcePassword", { passwordHash: varchar("passwordHash").notNull() }); +export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { + headerAuthId: serial("headerAuthId").primaryKey(), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + headerAuthHash: varchar("headerAuthHash").notNull() +}); + export const resourceAccessToken = pgTable("resourceAccessToken", { accessTokenId: varchar("accessTokenId").primaryKey(), orgId: varchar("orgId") @@ -689,6 +697,7 @@ export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceHeaderAuth = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index f7719c50..09c465b5 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -6,6 +6,8 @@ import { ResourceRule, resourcePassword, resourcePincode, + resourceHeaderAuth, + ResourceHeaderAuth, resourceRules, resources, roleResources, @@ -24,6 +26,7 @@ export type ResourceWithAuth = { resource: Resource | null; pincode: ResourcePincode | null; password: ResourcePassword | null; + headerAuth: ResourceHeaderAuth | null; }; export type UserSessionWithUser = { @@ -72,6 +75,10 @@ export async function getResourceByDomain( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourceId, resources.resourceId) + ) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -82,7 +89,8 @@ export async function getResourceByDomain( return { resource: result.resources, pincode: result.resourcePincode, - password: result.resourcePassword + password: result.resourcePassword, + headerAuth: result.resourceHeaderAuth }; } diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 62fca8b4..327b5d93 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -513,6 +513,16 @@ export const resourcePassword = sqliteTable("resourcePassword", { passwordHash: text("passwordHash").notNull() }); +export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { + headerAuthId: integer("headerAuthId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + headerAuthHash: text("headerAuthHash").notNull() +}); + export const resourceAccessToken = sqliteTable("resourceAccessToken", { accessTokenId: text("accessTokenId").primaryKey(), orgId: text("orgId") @@ -728,6 +738,7 @@ export type UserOrg = InferSelectModel; export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; +export type ResourceHeaderAuth = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 7a0139bb..c380e679 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -7,22 +7,21 @@ import { import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { getResourceByDomain, - getUserSessionWithUser, - getUserOrgRole, - getRoleResourceAccess, - getUserResourceAccess, getResourceRules, - getOrgLoginPage + getRoleResourceAccess, + getUserOrgRole, + getUserResourceAccess, + getOrgLoginPage, + getUserSessionWithUser } from "@server/db/queries/verifySessionQueries"; import { LoginPage, Resource, ResourceAccessToken, + ResourceHeaderAuth, ResourcePassword, ResourcePincode, - ResourceRule, - sessions, - users + ResourceRule } from "@server/db"; import config from "@server/lib/config"; import { isIpInCidr } from "@server/lib/ip"; @@ -37,6 +36,7 @@ import { fromError } from "zod-validation-error"; import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip"; import { getOrgTierData } from "@server/routers/private/billing"; import { TierId } from "@server/lib/private/billing/tiers"; +import { verifyPassword } from "@server/auth/password"; // We'll see if this speeds anything up const cache = new NodeCache({ @@ -101,25 +101,28 @@ export async function verifyResourceSession( query } = parsedBody.data; + // Extract HTTP Basic Auth credentials if present + const clientHeaderAuth = extractBasicAuth(headers); + const clientIp = requestIp ? (() => { - logger.debug("Request IP:", { requestIp }); - if (requestIp.startsWith("[") && requestIp.includes("]")) { - // if brackets are found, extract the IPv6 address from between the brackets - const ipv6Match = requestIp.match(/\[(.*?)\]/); - if (ipv6Match) { - return ipv6Match[1]; - } - } + logger.debug("Request IP:", { requestIp }); + if (requestIp.startsWith("[") && requestIp.includes("]")) { + // if brackets are found, extract the IPv6 address from between the brackets + const ipv6Match = requestIp.match(/\[(.*?)\]/); + if (ipv6Match) { + return ipv6Match[1]; + } + } - // ivp4 - // split at last colon - const lastColonIndex = requestIp.lastIndexOf(":"); - if (lastColonIndex !== -1) { - return requestIp.substring(0, lastColonIndex); - } - return requestIp; - })() + // ivp4 + // split at last colon + const lastColonIndex = requestIp.lastIndexOf(":"); + if (lastColonIndex !== -1) { + return requestIp.substring(0, lastColonIndex); + } + return requestIp; + })() : undefined; logger.debug("Client IP:", { clientIp }); @@ -134,10 +137,11 @@ export async function verifyResourceSession( const resourceCacheKey = `resource:${cleanHost}`; let resourceData: | { - resource: Resource | null; - pincode: ResourcePincode | null; - password: ResourcePassword | null; - } + resource: Resource | null; + pincode: ResourcePincode | null; + password: ResourcePassword | null; + headerAuth: ResourceHeaderAuth | null; + } | undefined = cache.get(resourceCacheKey); if (!resourceData) { @@ -152,7 +156,7 @@ export async function verifyResourceSession( cache.set(resourceCacheKey, resourceData); } - const { resource, pincode, password } = resourceData; + const { resource, pincode, password, headerAuth } = resourceData; if (!resource) { logger.debug(`Resource not found ${cleanHost}`); @@ -209,21 +213,21 @@ export async function verifyResourceSession( headers && headers[ config.getRawConfig().server.resource_access_token_headers.id - ] && + ] && headers[ config.getRawConfig().server.resource_access_token_headers.token - ] + ] ) { const accessTokenId = headers[ config.getRawConfig().server.resource_access_token_headers .id - ]; + ]; const accessToken = headers[ config.getRawConfig().server.resource_access_token_headers .token - ]; + ]; const { valid, error, tokenItem } = await verifyResourceAccessToken( { @@ -288,6 +292,18 @@ export async function verifyResourceSession( } } + // check for HTTP Basic Auth header + if (headerAuth && clientHeaderAuth) { + if(cache.get(clientHeaderAuth)) { + logger.debug("Resource allowed because header auth is valid (cached)"); + return allowed(res); + }else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){ + cache.set(clientHeaderAuth, clientHeaderAuth); + logger.debug("Resource allowed because header auth is valid"); + return allowed(res); + } + } + if (!sessions) { if (config.getRawConfig().app.log_failed_attempts) { logger.info( @@ -800,3 +816,25 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise { return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase(); } + +function extractBasicAuth(headers: Record | undefined): string | undefined { + if (!headers || (!headers.authorization && !headers.Authorization)) { + return; + } + + const authHeader = headers.authorization || headers.Authorization; + + // Check if it's Basic Auth + if (!authHeader.startsWith("Basic ")) { + logger.debug("Authorization header is not Basic Auth"); + return; + } + + try { + // Extract the base64 encoded credentials + return authHeader.slice("Basic ".length); + + } catch (error) { + logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" }); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index d6fa4a16..3a96bb03 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -541,6 +541,13 @@ authenticated.post( resource.setResourcePincode ); +authenticated.post( + `/resource/:resourceId/header-auth`, + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), + resource.setResourceHeaderAuth +); + authenticated.post( `/resource/:resourceId/whitelist`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index edad73b9..879075c7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -24,8 +24,7 @@ import { verifyApiKeyIsRoot, verifyApiKeyClientAccess, verifyClientsEnabled, - verifyApiKeySiteResourceAccess, - verifyOrgAccess + verifyApiKeySiteResourceAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -401,6 +400,13 @@ authenticated.post( resource.setResourcePincode ); +authenticated.post( + `/resource/:resourceId/header-auth`, + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth), + resource.setResourceHeaderAuth +); + authenticated.post( `/resource/:resourceId/whitelist`, verifyApiKeyResourceAccess, @@ -660,4 +666,4 @@ authenticated.put( verifyApiKeyOrgAccess, verifyApiKeyHasAction(ActionsEnum.applyBlueprint), org.applyBlueprint -); \ No newline at end of file +); diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index f6c8c596..b7775251 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -1,7 +1,12 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; -import { resourcePassword, resourcePincode, resources } from "@server/db"; +import { + db, + resourceHeaderAuth, + resourcePassword, + resourcePincode, + resources +} from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -23,6 +28,7 @@ export type GetResourceAuthInfoResponse = { niceId: string; password: boolean; pincode: boolean; + headerAuth: boolean; sso: boolean; blockAccess: boolean; url: string; @@ -64,6 +70,14 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) + + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) .where(eq(resources.resourceId, Number(resourceGuid))) .limit(1) : await db @@ -77,12 +91,21 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) + + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) .where(eq(resources.resourceGuid, resourceGuid)) .limit(1); const resource = result?.resources; const pincode = result?.resourcePincode; const password = result?.resourcePassword; + const headerAuth = result?.resourceHeaderAuth; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; @@ -100,6 +123,7 @@ export async function getResourceAuthInfo( resourceName: resource.name, password: password !== null, pincode: pincode !== null, + headerAuth: headerAuth !== null, sso: resource.sso, blockAccess: resource.blockAccess, url, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 1a2e5c2d..60938342 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -22,3 +22,4 @@ export * from "./deleteResourceRule"; export * from "./listResourceRules"; export * from "./updateResourceRule"; export * from "./getUserResources"; +export * from "./setResourceHeaderAuth"; diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts new file mode 100644 index 00000000..2e3b1a4a --- /dev/null +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -0,0 +1,101 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, resourceHeaderAuth } from "@server/db"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { response } from "@server/lib"; +import logger from "@server/logger"; +import { hashPassword } from "@server/auth/password"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourceAuthMethodsParamsSchema = z.object({ + resourceId: z.string().transform(Number).pipe(z.number().int().positive()) +}); + +const setResourceAuthMethodsBodySchema = z + .object({ + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable() + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/resource/{resourceId}/header-auth", + description: + "Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.", + tags: [OpenAPITags.Resource], + request: { + params: setResourceAuthMethodsParamsSchema, + body: { + content: { + "application/json": { + schema: setResourceAuthMethodsBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourceHeaderAuth( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourceAuthMethodsParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourceAuthMethodsBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + const { user, password } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourceHeaderAuth) + .where(eq(resourceHeaderAuth.resourceId, resourceId)); + + if (user && password) { + const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); + + await trx + .insert(resourceHeaderAuth) + .values({ resourceId, headerAuthHash }); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Header Authentication set successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index ae8e52ab..4b9e3ac1 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -13,7 +13,7 @@ import { ListResourceUsersResponse } from "@server/routers/resource"; import { Button } from "@app/components/ui/button"; -import { set, z } from "zod"; +import { z } from "zod"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { @@ -26,9 +26,10 @@ import { FormMessage } from "@app/components/ui/form"; import { ListUsersResponse } from "@server/routers/user"; -import { Binary, Key } from "lucide-react"; +import { Binary, Key, Bot } from "lucide-react"; import SetResourcePasswordForm from "../../../../../../components/SetResourcePasswordForm"; import SetResourcePincodeForm from "../../../../../../components/SetResourcePincodeForm"; +import SetResourceHeaderAuthForm from "../../../../../../components/SetResourceHeaderAuthForm"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { @@ -57,10 +58,13 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +<<<<<<< HEAD import { Separator } from "@app/components/ui/separator"; import { build } from "@server/build"; import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext"; import { TierId } from "@server/lib/private/billing/tiers"; +======= +>>>>>>> 6f6c351f (Adding HTTP Header Authentication) const UsersRolesFormSchema = z.object({ roles: z.array( @@ -140,9 +144,12 @@ export default function ResourceAuthenticationPage() { useState(false); const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = useState(false); + const [loadingRemoveResourceHeaderAuth, setLoadingRemoveResourceHeaderAuth] = + useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); const usersRolesForm = useForm({ resolver: zodResolver(UsersRolesFormSchema), @@ -429,6 +436,37 @@ export default function ResourceAuthenticationPage() { .finally(() => setLoadingRemoveResourcePincode(false)); } + function removeResourceHeaderAuth() { + setLoadingRemoveResourceHeaderAuth(true); + + api.post(`/resource/${resource.resourceId}/header-auth`, { + user: null, + password: null + }) + .then(() => { + toast({ + title: t("resourceHeaderAuthRemove"), + description: t("resourceHeaderAuthRemoveDescription") + }); + + updateAuthInfo({ + headerAuth: false + }); + router.refresh(); + }) + .catch((e) => { + toast({ + variant: "destructive", + title: t("resourceErrorHeaderAuthRemove"), + description: formatAxiosError( + e, + t("resourceErrorHeaderAuthRemoveDescription") + ) + }); + }) + .finally(() => setLoadingRemoveResourceHeaderAuth(false)); + } + if (pageLoading) { return <>; } @@ -463,6 +501,20 @@ export default function ResourceAuthenticationPage() { /> )} + {isSetHeaderAuthOpen && ( + { + setIsSetHeaderAuthOpen(false); + updateAuthInfo({ + headerAuth: true + }); + }} + /> + )} + @@ -778,6 +830,36 @@ export default function ResourceAuthenticationPage() { : t("pincodeAdd")} + + {/* Header Authentication Protection */} +
+
+ + + {t("resourceHeaderAuthProtection", { + status: authInfo.headerAuth + ? t("enabled") + : t("disabled") + })} + +
+ +
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 38723613..f7f895b3 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -57,6 +57,7 @@ function getActionsCategories(root: boolean) { [t('actionListAllowedResourceRoles')]: "listResourceRoles", [t('actionSetResourcePassword')]: "setResourcePassword", [t('actionSetResourcePincode')]: "setResourcePincode", + [t('actionSetResourceHeaderAuth')]: "setResourceHeaderAuth", [t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist", [t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist", [t('actionCreateSiteResource')]: "createSiteResource", diff --git a/src/components/SetResourceHeaderAuthForm.tsx b/src/components/SetResourceHeaderAuthForm.tsx new file mode 100644 index 00000000..b1a75543 --- /dev/null +++ b/src/components/SetResourceHeaderAuthForm.tsx @@ -0,0 +1,186 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { formatAxiosError } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { Resource } from "@server/db"; +import { createApiClient } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useTranslations } from "next-intl"; + +const setHeaderAuthFormSchema = z.object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100) +}); + +type SetHeaderAuthFormValues = z.infer; + +const defaultValues: Partial = { + user: "", + password: "" +}; + +type SetHeaderAuthFormProps = { + open: boolean; + setOpen: (open: boolean) => void; + resourceId: number; + onSetHeaderAuth?: () => void; +}; + +export default function SetResourceHeaderAuthForm({ + open, + setOpen, + resourceId, + onSetHeaderAuth +}: SetHeaderAuthFormProps) { + const api = createApiClient(useEnvContext()); + const t = useTranslations(); + + const [loading, setLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(setHeaderAuthFormSchema), + defaultValues + }); + + useEffect(() => { + if (!open) { + return; + } + + form.reset(); + }, [open]); + + async function onSubmit(data: SetHeaderAuthFormValues) { + setLoading(true); + + api.post>(`/resource/${resourceId}/header-auth`, { + user: data.user, + password: data.password + }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorHeaderAuthSetup'), + description: formatAxiosError( + e, + t('resourceErrorHeaderAuthSetupDescription') + ) + }); + }) + .then(() => { + toast({ + title: t('resourceHeaderAuthSetup'), + description: t('resourceHeaderAuthSetupDescription') + }); + + if (onSetHeaderAuth) { + onSetHeaderAuth(); + } + }) + .finally(() => setLoading(false)); + } + + return ( + <> + { + setOpen(val); + setLoading(false); + form.reset(); + }} + > + + + {t('resourceHeaderAuthSetupTitle')} + + {t('resourceHeaderAuthSetupTitleDescription')} + + + +
+ + ( + + {t('user')} + + + + + + )} + /> + ( + + {t('password')} + + + + + + )} + /> + + +
+ + + + + + +
+
+ + ); +}