diff --git a/bun.lockb b/bun.lockb index 21baeae..144415a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/packages/doorman-api/package.json b/packages/doorman-api/package.json index dfab9cd..aea0490 100644 --- a/packages/doorman-api/package.json +++ b/packages/doorman-api/package.json @@ -25,7 +25,8 @@ "winston-loki": "^6.1.3", "zod": "^3.25.42", "zod-validation-error": "^3.4.1", - "zod_utilz": "^0.8.4" + "zod_utilz": "^0.8.4", + "doorman-schema": "workspace:*" }, "devDependencies": { "twilio-run": "^3.5.4", diff --git a/packages/doorman-api/src/functions/api/door/auth.ts b/packages/doorman-api/src/functions/api/door/auth.ts index 896e3ca..308dc6f 100644 --- a/packages/doorman-api/src/functions/api/door/auth.ts +++ b/packages/doorman-api/src/functions/api/door/auth.ts @@ -13,17 +13,8 @@ import { AuthMetrics, registerMetrics } from "../../../metrics/AuthMetrics"; import { Counter } from "prom-client"; import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig"; import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus"; -import { z } from "zod"; import { setResponseJson } from "../../../utils/responseUtils"; - -export const AuthRequestSchema = z.object({ - door: z.string(), - key: z.string(), - ip: z.string().optional(), - timeout: z.coerce.number().gt(0, "Timeout cannot be 0").optional(), -}); - -export type AuthRequest = z.infer; +import { AuthRequest, AuthRequestSchema } from "../../../../../doorman-schema/src"; export interface AuthRequestTwilio extends ServerlessEventObject { }; diff --git a/packages/doorman-schema/package.json b/packages/doorman-schema/package.json new file mode 100644 index 0000000..9ff2901 --- /dev/null +++ b/packages/doorman-schema/package.json @@ -0,0 +1,9 @@ +{ + "name": "doorman-schema", + "module": "src/index.ts", + "type": "module", + "dependencies": { + "zod": "^3.25.56", + "zod_utilz": "^0.8.4" + } +} \ No newline at end of file diff --git a/packages/doorman-schema/src/index.ts b/packages/doorman-schema/src/index.ts new file mode 100644 index 0000000..fed0a87 --- /dev/null +++ b/packages/doorman-schema/src/index.ts @@ -0,0 +1 @@ +export * from "./routes/auth"; \ No newline at end of file diff --git a/packages/doorman-schema/src/routes/auth.ts b/packages/doorman-schema/src/routes/auth.ts new file mode 100644 index 0000000..e2e7db3 --- /dev/null +++ b/packages/doorman-schema/src/routes/auth.ts @@ -0,0 +1,10 @@ +import z from "zod"; + +export const AuthRequestSchema = z.object({ + door: z.string(), + key: z.string().min(1, "PIN cannot be empty"), + ip: z.string().optional(), + timeout: z.coerce.number().gt(0, "Timeout cannot be 0").optional(), +}); + +export type AuthRequest = z.infer; \ No newline at end of file diff --git a/packages/doorman-ui/package.json b/packages/doorman-ui/package.json index e412577..49eb5aa 100644 --- a/packages/doorman-ui/package.json +++ b/packages/doorman-ui/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@cloudscape-design/components": "^3.0.375", + "@hookform/resolvers": "^5.1.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", "@testing-library/user-event": "^13.2.1", @@ -22,7 +23,8 @@ "vite": "^4.4.9", "vite-plugin-svgr": "^4.0.0", "vite-tsconfig-paths": "^4.2.1", - "web-vitals": "^2.1.0" + "web-vitals": "^2.1.0", + "zod": "^3.25.56" }, "scripts": { "start": "vite", diff --git a/packages/doorman-ui/src/components/AuthComponent.tsx b/packages/doorman-ui/src/components/AuthComponent.tsx index 80f72af..1444d47 100644 --- a/packages/doorman-ui/src/components/AuthComponent.tsx +++ b/packages/doorman-ui/src/components/AuthComponent.tsx @@ -1,8 +1,12 @@ -import { Button, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components"; +import { Button, Form, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components"; import { useEffect, useState } from "react"; -import type { AuthRequest } from "../../../doorman-api/src/functions/api/door/auth"; +import { AuthRequest, AuthRequestSchema } from "../../../doorman-schema/src"; import { fetchUrlEncoded } from "../helpers/FetchHelper"; +import { useForm } from "react-hook-form"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import CInput from "react-hook-form-cloudscape/components/input"; // COPIED FROM SERVER export enum IAuthMode { @@ -10,7 +14,6 @@ export enum IAuthMode { RANDOM_ROTATING_KEY = "Random Rotating Key", } - export interface IAuthComponentProps { door: string, authMode: IAuthMode, @@ -21,17 +24,42 @@ export interface IAuthComponentProps { } export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => { - const [ key, setKey ] = useState(secret || ""); - const [ error, setError ] = useState(""); + const { control, setError, formState, handleSubmit, setValue, getValues } = useForm({ + resolver: zodResolver(AuthRequestSchema), + defaultValues: { + key: secret || "", + door, + } + }); + + const [isLoading, setLoading] = useState(false); + + const onSubmit = async (body: AuthRequest) => { + setLoading(true); + await fetchUrlEncoded('/api/door/auth', body) + .then(async res => { + if (res.status !== 200) { + setError("key", { type: "validate", message: "Incorrect PIN"}); + onError && onError(await res.json()); + return; + } + onUnlock(); + }); + setLoading(false); + }; useEffect(() => { - if (runCheck) { - onSubmit(); + // don't allow us to submit while we are submitting + if (runCheck && !isLoading) { + handleSubmit(onSubmit)(); } - }, [runCheck]) + }, [runCheck]); - const onSubmit = async () => { - const ip = await fetch('https://api.ipify.org?format=json') + + // try and submit on load if the pin is passed in by query params + useEffect(() => { + setLoading(true); + fetch('https://api.ipify.org?format=json') .then(response => response.json()) .then(data => { return data.ip; @@ -39,30 +67,24 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh .catch(error => { console.log('Error:', error); return "null"; - }); + }) + .then(async ip => { + setValue("ip", ip); - const body: AuthRequest = { - door, - ip, - key, - }; - - fetchUrlEncoded('/api/door/auth', body) - .then(async res => { - if (res.status !== 200) { - setError("Incorrect PIN"); - onError && onError(await res.json()); - return; + // if the key is there, then try submitting right away + if (secret !== null) { + await handleSubmit(onSubmit)(); } - onUnlock(); - }); - } + + setLoading(false); + }); + }, [secret]); return ( <> - - setKey(detail.value)} /> + + diff --git a/packages/doorman-ui/src/index.tsx b/packages/doorman-ui/src/index.tsx index f30daa7..32493b5 100644 --- a/packages/doorman-ui/src/index.tsx +++ b/packages/doorman-ui/src/index.tsx @@ -36,7 +36,5 @@ const router = createBrowserRouter([ ]) root.render( - - - + );