From ad5ea3564b07688df387efd0f9bceceaeebbb3f5 Mon Sep 17 00:00:00 2001 From: Milo Schwartz Date: Sat, 23 Nov 2024 20:08:56 -0500 Subject: [PATCH] added support for pin code auth --- server/routers/external.ts | 88 ++++---- server/routers/resource/authWithPincode.ts | 154 ++++++++++++++ server/routers/resource/index.ts | 2 + server/routers/resource/setResourcePincode.ts | 91 ++++++++ .../components/SetResourcePincodeForm.tsx | 199 ++++++++++++++++++ .../[resourceId]/authentication/page.tsx | 136 +++++++++--- src/app/auth/layout.tsx | 4 +- src/app/auth/login/DashboardLoginForm.tsx | 2 +- .../components/ResourceAccessDenied.tsx | 0 .../components/ResourceAuthPortal.tsx | 36 +++- .../components/ResourceNotFound.tsx | 0 .../auth/resource/[resourceId]/page.tsx | 25 +-- src/app/auth/signup/SignupForm.tsx | 2 +- src/app/auth/verify-email/VerifyEmailForm.tsx | 2 +- 14 files changed, 653 insertions(+), 88 deletions(-) create mode 100644 server/routers/resource/authWithPincode.ts create mode 100644 server/routers/resource/setResourcePincode.ts create mode 100644 src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx rename src/app/{[orgId] => }/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx (100%) rename src/app/{[orgId] => }/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx (91%) rename src/app/{[orgId] => }/auth/resource/[resourceId]/components/ResourceNotFound.tsx (100%) rename src/app/{[orgId] => }/auth/resource/[resourceId]/page.tsx (80%) diff --git a/server/routers/external.ts b/server/routers/external.ts index f207b431..142362bd 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -43,51 +43,51 @@ authenticated.get( "/org/:orgId", verifyOrgAccess, verifyUserHasAction(ActionsEnum.getOrg), - org.getOrg + org.getOrg, ); authenticated.post( "/org/:orgId", verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateOrg), - org.updateOrg + org.updateOrg, ); authenticated.delete( "/org/:orgId", verifyOrgAccess, verifyUserIsOrgOwner, - org.deleteOrg + org.deleteOrg, ); authenticated.put( "/org/:orgId/site", verifyOrgAccess, verifyUserHasAction(ActionsEnum.createSite), - site.createSite + site.createSite, ); authenticated.get( "/org/:orgId/sites", verifyOrgAccess, verifyUserHasAction(ActionsEnum.listSites), - site.listSites + site.listSites, ); authenticated.get( "/org/:orgId/site/:niceId", verifyOrgAccess, verifyUserHasAction(ActionsEnum.getSite), - site.getSite + site.getSite, ); authenticated.get( "/org/:orgId/pick-site-defaults", verifyOrgAccess, verifyUserHasAction(ActionsEnum.createSite), - site.pickSiteDefaults + site.pickSiteDefaults, ); authenticated.get( "/site/:siteId", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.getSite + site.getSite, ); // authenticated.get( // "/site/:siteId/roles", @@ -99,38 +99,38 @@ authenticated.post( "/site/:siteId", verifySiteAccess, verifyUserHasAction(ActionsEnum.updateSite), - site.updateSite + site.updateSite, ); authenticated.delete( "/site/:siteId", verifySiteAccess, verifyUserHasAction(ActionsEnum.deleteSite), - site.deleteSite + site.deleteSite, ); authenticated.put( "/org/:orgId/site/:siteId/resource", verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), - resource.createResource + resource.createResource, ); authenticated.get( "/site/:siteId/resources", verifyUserHasAction(ActionsEnum.listResources), - resource.listResources + resource.listResources, ); authenticated.get( "/org/:orgId/resources", verifyOrgAccess, verifyUserHasAction(ActionsEnum.listResources), - resource.listResources + resource.listResources, ); authenticated.post( "/org/:orgId/create-invite", verifyOrgAccess, verifyUserHasAction(ActionsEnum.inviteUser), - user.inviteUser + user.inviteUser, ); // maybe make this /invite/create instead authenticated.post("/invite/accept", user.acceptInvite); @@ -138,77 +138,77 @@ authenticated.get( "/resource/:resourceId/roles", verifyResourceAccess, verifyUserHasAction(ActionsEnum.listResourceRoles), - resource.listResourceRoles + resource.listResourceRoles, ); authenticated.get( "/resource/:resourceId/users", verifyResourceAccess, verifyUserHasAction(ActionsEnum.listResourceUsers), - resource.listResourceUsers + resource.listResourceUsers, ); authenticated.get( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.getResource), - resource.getResource + resource.getResource, ); authenticated.post( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResource), - resource.updateResource + resource.updateResource, ); authenticated.delete( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResource), - resource.deleteResource + resource.deleteResource, ); authenticated.put( "/resource/:resourceId/target", verifyResourceAccess, verifyUserHasAction(ActionsEnum.createTarget), - target.createTarget + target.createTarget, ); authenticated.get( "/resource/:resourceId/targets", verifyResourceAccess, verifyUserHasAction(ActionsEnum.listTargets), - target.listTargets + target.listTargets, ); authenticated.get( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.getTarget), - target.getTarget + target.getTarget, ); authenticated.post( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.updateTarget), - target.updateTarget + target.updateTarget, ); authenticated.delete( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.deleteTarget), - target.deleteTarget + target.deleteTarget, ); authenticated.put( "/org/:orgId/role", verifyOrgAccess, verifyUserHasAction(ActionsEnum.createRole), - role.createRole + role.createRole, ); authenticated.get( "/org/:orgId/roles", verifyOrgAccess, verifyUserHasAction(ActionsEnum.listRoles), - role.listRoles + role.listRoles, ); // authenticated.get( // "/role/:roleId", @@ -227,14 +227,14 @@ authenticated.delete( "/role/:roleId", verifyRoleAccess, verifyUserHasAction(ActionsEnum.deleteRole), - role.deleteRole + role.deleteRole, ); authenticated.post( "/role/:roleId/add/:userId", verifyRoleAccess, verifyUserAccess, verifyUserHasAction(ActionsEnum.addUserRole), - user.addUserRole + user.addUserRole, ); // authenticated.put( @@ -264,7 +264,7 @@ authenticated.post( verifyResourceAccess, verifyRoleAccess, verifyUserHasAction(ActionsEnum.setResourceRoles), - resource.setResourceRoles + resource.setResourceRoles, ); authenticated.post( @@ -272,19 +272,29 @@ authenticated.post( verifyResourceAccess, verifySetResourceUsers, verifyUserHasAction(ActionsEnum.setResourceUsers), - resource.setResourceUsers + resource.setResourceUsers, ); authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceAuthMethods), - resource.setResourcePassword + resource.setResourcePassword, ); - unauthenticated.post( "/resource/:resourceId/auth/password", - resource.authWithPassword + resource.authWithPassword, +); + +authenticated.post( + `/resource/:resourceId/pincode`, + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.setResourceAuthMethods), + resource.setResourcePincode, +); +unauthenticated.post( + "/resource/:resourceId/auth/pincode", + resource.authWithPincode, ); unauthenticated.get("/resource/:resourceId/auth", resource.getResourceAuthInfo); @@ -325,14 +335,14 @@ authenticated.get( "/org/:orgId/users", verifyOrgAccess, verifyUserHasAction(ActionsEnum.listUsers), - user.listUsers + user.listUsers, ); authenticated.delete( "/org/:orgId/user/:userId", verifyOrgAccess, verifyUserAccess, verifyUserHasAction(ActionsEnum.removeUser), - user.removeUserOrg + user.removeUserOrg, ); // authenticated.put( @@ -374,7 +384,7 @@ authRouter.use( windowMin: 10, max: 15, type: "IP_AND_PATH", - }) + }), ); authRouter.put("/signup", auth.signup); @@ -386,19 +396,19 @@ authRouter.post("/2fa/enable", verifySessionUserMiddleware, auth.verifyTotp); authRouter.post( "/2fa/request", verifySessionUserMiddleware, - auth.requestTotpSecret + auth.requestTotpSecret, ); authRouter.post("/2fa/disable", verifySessionUserMiddleware, auth.disable2fa); authRouter.post("/verify-email", verifySessionMiddleware, auth.verifyEmail); authRouter.post( "/verify-email/request", verifySessionMiddleware, - auth.requestEmailVerificationCode + auth.requestEmailVerificationCode, ); authRouter.post( "/change-password", verifySessionUserMiddleware, - auth.changePassword + auth.changePassword, ); authRouter.post("/reset-password/request", auth.requestPasswordReset); authRouter.post("/reset-password/", auth.resetPassword); diff --git a/server/routers/resource/authWithPincode.ts b/server/routers/resource/authWithPincode.ts new file mode 100644 index 00000000..1fb0538f --- /dev/null +++ b/server/routers/resource/authWithPincode.ts @@ -0,0 +1,154 @@ +import { verify } from "@node-rs/argon2"; +import { generateSessionToken } from "@server/auth"; +import db from "@server/db"; +import { resourcePincode, resources } from "@server/db/schema"; +import HttpCode from "@server/types/HttpCode"; +import response from "@server/utils/response"; +import { eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; +import { + createResourceSession, + serializeResourceSessionCookie, +} from "@server/auth/resource"; +import logger from "@server/logger"; + +export const authWithPincodeBodySchema = z.object({ + pincode: z.string(), + email: z.string().email().optional(), + code: z.string().optional(), +}); + +export const authWithPincodeParamsSchema = z.object({ + resourceId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +export type AuthWithPincodeResponse = { + codeRequested?: boolean; +}; + +export async function authWithPincode( + req: Request, + res: Response, + next: NextFunction, +): Promise { + const parsedBody = authWithPincodeBodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString(), + ), + ); + } + + const parsedParams = authWithPincodeParamsSchema.safeParse(req.params); + + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString(), + ), + ); + } + + const { resourceId } = parsedParams.data; + const { email, pincode, code } = parsedBody.data; + + try { + const [result] = await db + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId), + ) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + const resource = result?.resources; + const definedPincode = result?.resourcePincode; + + if (!resource) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Resource does not exist", + ), + ); + } + + if (!definedPincode) { + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + createHttpError( + HttpCode.BAD_REQUEST, + "Resource has no pincode protection", + ), + ), + ); + } + + const validPincode = await verify(definedPincode.pincodeHash, pincode, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + if (!validPincode) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Incorrect PIN code"), + ); + } + + if (resource.twoFactorEnabled) { + if (!code) { + return response(res, { + data: { codeRequested: true }, + success: true, + error: false, + message: "Two-factor authentication required", + status: HttpCode.ACCEPTED, + }); + } + + // TODO: Implement email OTP for resource 2fa + } + + const token = generateSessionToken(); + await createResourceSession({ + resourceId, + token, + pincodeId: definedPincode.pincodeId, + }); + const secureCookie = resource.ssl; + const cookie = serializeResourceSessionCookie( + token, + resource.fullDomain, + secureCookie, + ); + res.appendHeader("Set-Cookie", cookie); + + logger.debug(cookie); // remove after testing + + return response(res, { + data: null, + success: true, + error: false, + message: "Authenticated with resource successfully", + status: HttpCode.OK, + }); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to authenticate with resource", + ), + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 2595c57b..bcad882c 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -10,3 +10,5 @@ export * from "./listResourceUsers"; export * from "./setResourcePassword"; export * from "./authWithPassword"; export * from "./getResourceAuthInfo"; +export * from "./setResourcePincode"; +export * from "./authWithPincode"; diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts new file mode 100644 index 00000000..865f7e87 --- /dev/null +++ b/server/routers/resource/setResourcePincode.ts @@ -0,0 +1,91 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourcePincode } from "@server/db/schema"; +import { eq } from "drizzle-orm"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import { hash } from "@node-rs/argon2"; +import { response } from "@server/utils"; +import stoi from "@server/utils/stoi"; + +const setResourceAuthMethodsParamsSchema = z.object({ + resourceId: z.string().transform(Number).pipe(z.number().int().positive()), +}); + +const setResourceAuthMethodsBodySchema = z + .object({ + pincode: z + .string() + .regex(/^\d{6}$/) + .or(z.null()), + }) + .strict(); + +export async function setResourcePincode( + 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 { pincode } = parsedBody.data; + + await db.transaction(async (trx) => { + await trx + .delete(resourcePincode) + .where(eq(resourcePincode.resourceId, resourceId)); + + if (pincode) { + const pincodeHash = await hash(pincode, { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + await trx + .insert(resourcePincode) + .values({ resourceId, pincodeHash, digitLength: 6 }); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Resource PIN code set successfully", + status: HttpCode.CREATED, + }); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred", + ), + ); + } +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx new file mode 100644 index 00000000..f0d9a5fa --- /dev/null +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/components/SetResourcePincodeForm.tsx @@ -0,0 +1,199 @@ +"use client"; + +import api from "@app/api"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { useToast } 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/utils"; +import { AxiosResponse } from "axios"; +import { Resource } from "@server/db/schema"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot, +} from "@app/components/ui/input-otp"; + +const setPincodeFormSchema = z.object({ + pincode: z.string().length(6), +}); + +type SetPincodeFormValues = z.infer; + +const defaultValues: Partial = { + pincode: "", +}; + +type SetPincodeFormProps = { + open: boolean; + setOpen: (open: boolean) => void; + resourceId: number; + onSetPincode?: () => void; +}; + +export default function SetResourcePincodeForm({ + open, + setOpen, + resourceId, + onSetPincode, +}: SetPincodeFormProps) { + const { toast } = useToast(); + + const [loading, setLoading] = useState(false); + + const form = useForm({ + resolver: zodResolver(setPincodeFormSchema), + defaultValues, + }); + + useEffect(() => { + if (!open) { + return; + } + + form.reset(); + }, [open]); + + async function onSubmit(data: SetPincodeFormValues) { + setLoading(true); + + api.post>(`/resource/${resourceId}/pincode`, { + pincode: data.pincode, + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error setting resource PIN code", + description: formatAxiosError( + e, + "An error occurred while setting the resource PIN code", + ), + }); + }) + .then(() => { + toast({ + title: "Resource PIN code set", + description: + "The resource pincode has been set successfully", + }); + + if (onSetPincode) { + onSetPincode(); + } + }) + .finally(() => setLoading(false)); + } + + return ( + <> + { + setOpen(val); + setLoading(false); + form.reset(); + }} + > + + + Set Pincode + + Set a pincode to protect this resource + + + +
+ + ( + + PIN Code + +
+ + + + + + + + + + +
+
+ + Users will be able to access + this resource by entering this + PIN code. It must be at least 6 + digits long. + + +
+ )} + /> + + +
+ + + + + + +
+
+ + ); +} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index d4d983b4..e1eaa509 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -32,9 +32,10 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { ListUsersResponse } from "@server/routers/user"; import { Switch } from "@app/components/ui/switch"; import { Label } from "@app/components/ui/label"; -import { ShieldCheck } from "lucide-react"; +import { Binary, Key, ShieldCheck } from "lucide-react"; import SetResourcePasswordForm from "./components/SetResourcePasswordForm"; import { Separator } from "@app/components/ui/separator"; +import SetResourcePincodeForm from "./components/SetResourcePincodeForm"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -78,8 +79,11 @@ export default function ResourceAuthenticationPage() { const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = useState(false); + const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = + useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); + const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const usersRolesForm = useForm>({ resolver: zodResolver(UsersRolesFormSchema), @@ -237,6 +241,36 @@ export default function ResourceAuthenticationPage() { .finally(() => setLoadingRemoveResourcePassword(false)); } + function removeResourcePincode() { + setLoadingRemoveResourcePincode(true); + + api.post(`/resource/${resource.resourceId}/pincode`, { + pincode: null, + }) + .then(() => { + toast({ + title: "Resource pincode removed", + description: + "The resource password has been removed successfully", + }); + + updateAuthInfo({ + pincode: false, + }); + }) + .catch((e) => { + toast({ + variant: "destructive", + title: "Error removing resource pincode", + description: formatAxiosError( + e, + "An error occurred while removing the resource pincode", + ), + }); + }) + .finally(() => setLoadingRemoveResourcePincode(false)); + } + if (pageLoading) { return <>; } @@ -257,6 +291,20 @@ export default function ResourceAuthenticationPage() { /> )} + {isSetPincodeOpen && ( + { + setIsSetPincodeOpen(false); + updateAuthInfo({ + pincode: true, + }); + }} + /> + )} +
-
+
- {authInfo?.password ? ( -
-
- - Password Protection Enabled +
+
+
+ + + Password Protection{" "} + {authInfo?.password + ? "Enabled" + : "Disabled"} +
- + {authInfo?.password ? ( + + ) : ( + + )}
- ) : ( -
- + + + PIN Code Protection{" "} + {authInfo?.pincode ? "Enabled" : "Disabled"} + +
+ {authInfo?.pincode ? ( + + ) : ( + + )}
- )} +
diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 24bc8c11..f7efced2 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -12,7 +12,9 @@ type AuthLayoutProps = { export default async function AuthLayout({ children }: AuthLayoutProps) { return ( <> -
{children}
+
+ {children} +
); } diff --git a/src/app/auth/login/DashboardLoginForm.tsx b/src/app/auth/login/DashboardLoginForm.tsx index 56e08392..986993cd 100644 --- a/src/app/auth/login/DashboardLoginForm.tsx +++ b/src/app/auth/login/DashboardLoginForm.tsx @@ -20,7 +20,7 @@ export default function DashboardLoginForm({ const router = useRouter(); return ( - + Login diff --git a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx similarity index 100% rename from src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx rename to src/app/auth/resource/[resourceId]/components/ResourceAccessDenied.tsx diff --git a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx similarity index 91% rename from src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx rename to src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx index bfb6fa4d..ba146587 100644 --- a/src/app/[orgId]/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx +++ b/src/app/auth/resource/[resourceId]/components/ResourceAuthPortal.tsx @@ -68,7 +68,9 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const [passwordError, setPasswordError] = useState(null); + const [pincodeError, setPincodeError] = useState(null); const [accessDenied, setAccessDenied] = useState(false); + const [loadingLogin, setLoadingLogin] = useState(false); function getDefaultSelectedMethod() { if (props.methods.sso) { @@ -111,11 +113,24 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { }); const onPinSubmit = (values: z.infer) => { - console.log("PIN authentication", values); - // Implement PIN authentication logic here + setLoadingLogin(true); + api.post(`/resource/${props.resource.id}/auth/pincode`, { + pincode: values.pin, + }) + .then((res) => { + window.location.href = props.redirect; + }) + .catch((e) => { + console.error(e); + setPincodeError( + formatAxiosError(e, "Failed to authenticate with pincode"), + ); + }) + .then(() => setLoadingLogin(false)); }; const onPasswordSubmit = (values: z.infer) => { + setLoadingLogin(true); api.post(`/resource/${props.resource.id}/auth/password`, { password: values.password, }) @@ -127,7 +142,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { setPasswordError( formatAxiosError(e, "Failed to authenticate with password"), ); - }); + }) + .finally(() => setLoadingLogin(false)); }; async function handleSSOAuth() { @@ -202,8 +218,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { render={({ field }) => ( - Enter 6-digit - PIN + 6-digit PIN Code
@@ -252,9 +267,18 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { )} /> + {pincodeError && ( + + + {pincodeError} + + + )}