seahaven-door-unlock-api/lambda/unlock-handler.ts

90 lines
3.2 KiB
TypeScript

import {
SSMClient,
GetParameterCommand,
} from "@aws-sdk/client-ssm";
const ssm = new SSMClient({});
let cachedAuthToken: string | undefined;
let cachedApiKey: string | undefined;
let cachedDoorId: string | undefined;
let lastUnlockTime = 0;
const COOLDOWN_MS = 5_000;
async function getParameter(name: string, decrypt: boolean): Promise<string> {
const res = await ssm.send(
new GetParameterCommand({ Name: name, WithDecryption: decrypt })
);
return res.Parameter!.Value!;
}
async function loadSecrets() {
const [authToken, apiKey, doorId] = await Promise.all([
cachedAuthToken ?? getParameter(process.env.AUTH_TOKEN_PARAM!, true),
cachedApiKey ?? getParameter(process.env.ELEMENTS_API_KEY_PARAM!, true),
cachedDoorId ?? getParameter(process.env.DOOR_ID_PARAM!, false),
]);
cachedAuthToken = authToken;
cachedApiKey = apiKey;
cachedDoorId = doorId;
return { authToken, apiKey, doorId };
}
export async function handler(event: {
queryStringParameters?: Record<string, string>;
requestContext?: { http?: { sourceIp?: string } };
}) {
const sourceIp = event.requestContext?.http?.sourceIp ?? "unknown";
const token = event.queryStringParameters?.token;
if (!token) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "missing_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}
let secrets;
try {
secrets = await loadSecrets();
} catch (err) {
console.error(JSON.stringify({ action: "unlock_attempt", status: "error", reason: "ssm_failure", sourceIp, error: String(err) }));
return { statusCode: 500, body: JSON.stringify({ error: "Internal error" }) };
}
if (token !== secrets.authToken) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "invalid_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}
const now = Date.now();
if (now - lastUnlockTime < COOLDOWN_MS) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "cooldown", sourceIp }));
return { statusCode: 429, body: JSON.stringify({ message: "Cooldown active, try again shortly" }) };
}
const url = `https://api.elementssecure.com/v1/devices/${secrets.doorId}/commands/TemporaryUnlock/execute`;
try {
const response = await fetch(url, {
method: "POST",
headers: {
"api-key": secrets.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!response.ok) {
const body = await response.text();
console.error(JSON.stringify({ action: "unlock_attempt", status: "elements_error", statusCode: response.status, body, sourceIp }));
return { statusCode: 502, body: JSON.stringify({ error: "Door system error" }) };
}
lastUnlockTime = now;
console.log(JSON.stringify({ action: "unlock_attempt", status: "success", sourceIp }));
return { statusCode: 200, body: JSON.stringify({ message: "Door unlocked" }) };
} catch (err) {
console.error(JSON.stringify({ action: "unlock_attempt", status: "error", reason: "elements_unreachable", sourceIp, error: String(err) }));
return { statusCode: 502, body: JSON.stringify({ error: "Door system unreachable" }) };
}
}