create new package for schemas and use it for auth

This commit is contained in:
Martin Dimitrov 2025-06-08 12:54:54 -07:00
parent 11827c30c1
commit b68daf7312
9 changed files with 77 additions and 43 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -25,7 +25,8 @@
"winston-loki": "^6.1.3", "winston-loki": "^6.1.3",
"zod": "^3.25.42", "zod": "^3.25.42",
"zod-validation-error": "^3.4.1", "zod-validation-error": "^3.4.1",
"zod_utilz": "^0.8.4" "zod_utilz": "^0.8.4",
"doorman-schema": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"twilio-run": "^3.5.4", "twilio-run": "^3.5.4",

View File

@ -13,17 +13,8 @@ import { AuthMetrics, registerMetrics } from "../../../metrics/AuthMetrics";
import { Counter } from "prom-client"; import { Counter } from "prom-client";
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig"; import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus"; import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
import { z } from "zod";
import { setResponseJson } from "../../../utils/responseUtils"; import { setResponseJson } from "../../../utils/responseUtils";
import { AuthRequest, AuthRequestSchema } from "../../../../../doorman-schema/src";
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<typeof AuthRequestSchema>;
export interface AuthRequestTwilio extends ServerlessEventObject<AuthRequest, UserAgentHeader> { }; export interface AuthRequestTwilio extends ServerlessEventObject<AuthRequest, UserAgentHeader> { };

View File

@ -0,0 +1,9 @@
{
"name": "doorman-schema",
"module": "src/index.ts",
"type": "module",
"dependencies": {
"zod": "^3.25.56",
"zod_utilz": "^0.8.4"
}
}

View File

@ -0,0 +1 @@
export * from "./routes/auth";

View File

@ -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<typeof AuthRequestSchema>;

View File

@ -4,6 +4,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@cloudscape-design/components": "^3.0.375", "@cloudscape-design/components": "^3.0.375",
"@hookform/resolvers": "^5.1.0",
"@testing-library/jest-dom": "^5.14.1", "@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^13.0.0", "@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1", "@testing-library/user-event": "^13.2.1",
@ -22,7 +23,8 @@
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-svgr": "^4.0.0", "vite-plugin-svgr": "^4.0.0",
"vite-tsconfig-paths": "^4.2.1", "vite-tsconfig-paths": "^4.2.1",
"web-vitals": "^2.1.0" "web-vitals": "^2.1.0",
"zod": "^3.25.56"
}, },
"scripts": { "scripts": {
"start": "vite", "start": "vite",

View File

@ -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 { 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 { 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 // COPIED FROM SERVER
export enum IAuthMode { export enum IAuthMode {
@ -10,7 +14,6 @@ export enum IAuthMode {
RANDOM_ROTATING_KEY = "Random Rotating Key", RANDOM_ROTATING_KEY = "Random Rotating Key",
} }
export interface IAuthComponentProps { export interface IAuthComponentProps {
door: string, door: string,
authMode: IAuthMode, authMode: IAuthMode,
@ -21,17 +24,42 @@ export interface IAuthComponentProps {
} }
export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => { export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => {
const [ key, setKey ] = useState(secret || ""); const { control, setError, formState, handleSubmit, setValue, getValues } = useForm<AuthRequest>({
const [ error, setError ] = useState(""); 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(() => { useEffect(() => {
if (runCheck) { // don't allow us to submit while we are submitting
onSubmit(); 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(response => response.json())
.then(data => { .then(data => {
return data.ip; return data.ip;
@ -39,30 +67,24 @@ export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCh
.catch(error => { .catch(error => {
console.log('Error:', error); console.log('Error:', error);
return "null"; return "null";
}); })
.then(async ip => {
setValue("ip", ip);
const body: AuthRequest = { // if the key is there, then try submitting right away
door, if (secret !== null) {
ip, await handleSubmit(onSubmit)();
key,
};
fetchUrlEncoded('/api/door/auth', body)
.then(async res => {
if (res.status !== 200) {
setError("Incorrect PIN");
onError && onError(await res.json());
return;
} }
onUnlock();
}); setLoading(false);
} });
}, [secret]);
return ( return (
<> <>
<SpaceBetween size='l'> <SpaceBetween size='l'>
<FormField errorText={error} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}> <FormField errorText={formState.errors.key?.message} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}>
<Input readOnly={secret !== null} value={key || ""} onChange={({ detail }) => setKey(detail.value)} /> <CInput type="password" name="key" readOnly={secret !== null} control={control} />
</FormField> </FormField>
</SpaceBetween> </SpaceBetween>
</> </>

View File

@ -36,7 +36,5 @@ const router = createBrowserRouter([
]) ])
root.render( root.render(
<React.StrictMode> <RouterProvider router={router} />
<RouterProvider router={router} />
</React.StrictMode>
); );