Compare commits

...

3 Commits

Author SHA1 Message Date
aa1de04fe7 add schema to workspace
Some checks failed
Build and push image for doorman-homeassistant / docker (push) Failing after 12s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Has been skipped
Build and push Doorman UI / API / docker (push) Failing after 36s
2025-06-08 13:02:52 -07:00
b5a0c467bd skip tsc 2025-06-08 13:01:32 -07:00
b68daf7312 create new package for schemas and use it for auth 2025-06-08 12:54:54 -07:00
10 changed files with 82 additions and 44 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -12,6 +12,10 @@
"name": "doorman-client",
"path": "packages/doorman-client"
},
{
"name": "doorman-schema",
"path": "packages/doorman-schema"
},
{
"name": "doorman",
"path": "."

View File

@ -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",

View File

@ -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<typeof AuthRequestSchema>;
import { AuthRequest, AuthRequestSchema } from "../../../../../doorman-schema/src";
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,
"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,11 +23,12 @@
"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",
"build": "tsc && vite build",
"build": "vite build",
"serve": "vite preview"
},
"eslintConfig": {

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 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<AuthRequest>({
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 (
<>
<SpaceBetween size='l'>
<FormField errorText={error} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}>
<Input readOnly={secret !== null} value={key || ""} onChange={({ detail }) => setKey(detail.value)} />
<FormField errorText={formState.errors.key?.message} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}>
<CInput type="password" name="key" readOnly={secret !== null} control={control} />
</FormField>
</SpaceBetween>
</>

View File

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