Compare commits

..

No commits in common. "aa1de04fe78f7ab9dd979b2712d1320d1b8fe2c6" and "11827c30c154287834305af65f7bdb979ffedf41" have entirely different histories.

10 changed files with 45 additions and 83 deletions

BIN
bun.lockb

Binary file not shown.

View File

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

View File

@ -25,8 +25,7 @@
"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,8 +13,17 @@ 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

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

View File

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

View File

@ -1,10 +0,0 @@
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,7 +4,6 @@
"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",
@ -23,12 +22,11 @@
"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",
"build": "vite build", "build": "tsc && vite build",
"serve": "vite preview" "serve": "vite preview"
}, },
"eslintConfig": { "eslintConfig": {

View File

@ -1,12 +1,8 @@
import { Button, Form, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components"; import { Button, FormField, Icon, Input, SpaceBetween } from "@cloudscape-design/components";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { AuthRequest, AuthRequestSchema } from "../../../doorman-schema/src"; import type { AuthRequest } from "../../../doorman-api/src/functions/api/door/auth";
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 {
@ -14,6 +10,7 @@ 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,
@ -24,42 +21,17 @@ export interface IAuthComponentProps {
} }
export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => { export const AuthComponent = ({ door, secret, authMode, onError, onUnlock, runCheck }: IAuthComponentProps) => {
const { control, setError, formState, handleSubmit, setValue, getValues } = useForm<AuthRequest>({ const [ key, setKey ] = useState(secret || "");
resolver: zodResolver(AuthRequestSchema), const [ error, setError ] = useState("");
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(() => {
// don't allow us to submit while we are submitting if (runCheck) {
if (runCheck && !isLoading) { onSubmit();
handleSubmit(onSubmit)();
} }
}, [runCheck]); }, [runCheck])
const onSubmit = async () => {
// try and submit on load if the pin is passed in by query params const ip = await fetch('https://api.ipify.org?format=json')
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;
@ -67,24 +39,30 @@ 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);
// if the key is there, then try submitting right away
if (secret !== null) {
await handleSubmit(onSubmit)();
}
setLoading(false);
}); });
}, [secret]);
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;
}
onUnlock();
});
}
return ( return (
<> <>
<SpaceBetween size='l'> <SpaceBetween size='l'>
<FormField errorText={formState.errors.key?.message} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}> <FormField errorText={error} label={"PIN"} constraintText={"Enter the PIN to unlock the door"}>
<CInput type="password" name="key" readOnly={secret !== null} control={control} /> <Input readOnly={secret !== null} value={key || ""} onChange={({ detail }) => setKey(detail.value)} />
</FormField> </FormField>
</SpaceBetween> </SpaceBetween>
</> </>

View File

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