mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 09:23:11 +00:00
Add a new Lambda handler that toggles Elements lockdown profiles (Bohemia and Ronkonkoma) via the Elements API, with status verification before and after each toggle. Returns Yealink XML to control linekey LEDs (green=inactive, red=locked down). Also brings both Lambda handlers into compliance with system standards: Node 22.x runtime, arm64 architecture, 60-day log retention, and kebab-case function names.
90 lines
3.2 KiB
TypeScript
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" }) };
|
|
}
|
|
}
|