From 25d38f2cc42fc74f470742c7b3719e0937be6fda Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 28 Jun 2026 13:20:59 -0400 Subject: [PATCH 01/11] feat: add query by date range --- routes/logs.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/routes/logs.js b/routes/logs.js index 326e312..f25429d 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -2,17 +2,23 @@ import { Router } from "express"; const router = Router(); router.get("/", async (req, res) => { - const cursor = req.query.cursor; - const query = cursor ? { timestamp: { $lt: new Date(cursor) } } : {}; + const { cursor, since, until } = req.query; + const query = {}; + if (since || until || cursor) { + query.timestamp = {}; + if (cursor) query.timestamp.$lt = new Date(cursor); + if (since) query.timestamp.$gte = new Date(since); + if (until) query.timestamp.$lte = new Date(until); + } const logs = await req.ctx.db .collection("accessLogs") .find(query) .sort({ timestamp: -1 }) - .limit(100) + .limit(50) .toArray(); - const nextCursor = logs.length > 0 + const nextCursor = logs.length === 50 ? logs[logs.length - 1].timestamp.toISOString() : null; From 1cb97220ad4c6a2ba88b7b1729505a8f0022046b Mon Sep 17 00:00:00 2001 From: aln730 Date: Wed, 8 Jul 2026 20:22:00 -0400 Subject: [PATCH 02/11] feat: keys by user, and more --- routes/keys.js | 216 ++++++++++++++++++++------------------- routes/memberProjects.js | 1 + server.js | 2 +- 3 files changed, 114 insertions(+), 105 deletions(-) diff --git a/routes/keys.js b/routes/keys.js index 886d287..8b1e57d 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,123 +1,131 @@ -import { Router } from "express"; -import crypto from "crypto"; -import { REALM_NAMES } from "../constants.js"; + import { Router } from "express"; + import crypto from "crypto"; + import { REALM_NAMES } from "../constants.js"; -const router = Router(); + const router = Router(); -// First, PUT /keys with details of user key is for -// Receive a keyId back which is our association -// Register key using association and send back the now-randomised UID -// with PATCH /keys/:id + router.get("/by-user", async (req, res) => { + if (typeof req.query.userId != "string") { + return res.status(422).json({ message: "Missing 'userId' query param" }); + } + const keys = await req.ctx.db.collection("keys").find({ userId: req.query.userId }).toArray(); + res.json(keys); + }); -router.put("/", async (req, res) => { - console.log(req.body); - if (typeof req.body.userId != "string") { - res.status(422).json({ - message: "No 'userId' field specified", - }); - return; - } - if (typeof req.body.uid != "string") { - res.status(422).json({ - message: "No 'uid' field specified", - }); - return; - } + // First, PUT /keys with details of user key is for + // Receive a keyId back which is our association + // Register key using association and send back the now-randomised UID + // with PATCH /keys/:id - const keys = {}; - for (const name of REALM_NAMES) { - keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); - } + router.put("/", async (req, res) => { + console.log(req.body); + if (typeof req.body.userId != "string") { + res.status(422).json({ + message: "No 'userId' field specified", + }); + return; + } + if (typeof req.body.uid != "string") { + res.status(422).json({ + message: "No 'uid' field specified", + }); + return; + } - const insertedKey = await req.ctx.db.collection("keys").insertOne({ - // Make sure it's something at least reasonable... - _id: crypto.randomBytes(18).toString("hex"), - userId: req.body.userId, - uid: req.body.uid, - // Not created yet, so we'll just leave it disabled for now - enabled: false, + const keys = {}; + for (const name of REALM_NAMES) { + keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); + } - ...keys, - }); - res.json({ - keyId: insertedKey.insertedId, - uid: req.body.uid, - ...keys, + const insertedKey = await req.ctx.db.collection("keys").insertOne({ + // Make sure it's something at least reasonable... + _id: crypto.randomBytes(18).toString("hex"), + userId: req.body.userId, + uid: req.body.uid, + // Not created yet, so we'll just leave it disabled for now + enabled: false, + + ...keys, + }); + res.json({ + keyId: insertedKey.insertedId, + uid: req.body.uid, + ...keys, + }); }); -}); -router.patch("/:id", async (req, res) => { - const updates = {}; - for (const key of ["uid", "userId", "enabled"]) { - if (key in req.body) { - updates[key] = req.body[key]; - } - } - await req.ctx.db.collection("keys").updateOne( - { - _id: {$eq: req.params.id}, - }, - { - $set: updates, + router.patch("/:id", async (req, res) => { + const updates = {}; + for (const key of ["uid", "userId", "enabled"]) { + if (key in req.body) { + updates[key] = req.body[key]; + } } - ); - res.status(204).send(null); -}); - -router.get("/by-association/:id", async (req, res) => { - const key = await req.ctx.db.collection("keys").findOne({ - $or: REALM_NAMES.map((key) => ({ - [key + "Id"]: { - $eq: req.params.id, + await req.ctx.db.collection("keys").updateOne( + { + _id: {$eq: req.params.id}, }, - })), + { + $set: updates, + } + ); + res.status(204).send(null); }); - if (key) { - const data = { - keyId: key._id, - uid: key.uid, - }; - for (const name of REALM_NAMES) { - data[name + "Id"] = key[name + "Id"]; - } - return res.json(data); - } else { - return res.status(404).json({ - message: "No such key!", + router.get("/by-association/:id", async (req, res) => { + const key = await req.ctx.db.collection("keys").findOne({ + $or: REALM_NAMES.map((key) => ({ + [key + "Id"]: { + $eq: req.params.id, + }, + })), }); - } -}); -router.delete("/by-user", async (req, res) => { - if (typeof req.body.userId != "string") { - return res.status(422).json({ - message: "Missing 'userId'", - }); - } - const results = await req.ctx.db.collection("keys").deleteMany({ - userId: {$eq: req.body.userId}, + if (key) { + const data = { + keyId: key._id, + uid: key.uid, + }; + for (const name of REALM_NAMES) { + data[name + "Id"] = key[name + "Id"]; + } + return res.json(data); + } else { + return res.status(404).json({ + message: "No such key!", + }); + } }); - if (results.deletedCount) { - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", - }); - } -}); -router.delete("/:keyId", async (req, res) => { - const results = await req.ctx.db.collection("keys").deleteOne({ - _id: {$eq: req.params.keyId}, + router.delete("/by-user", async (req, res) => { + if (typeof req.body.userId != "string") { + return res.status(422).json({ + message: "Missing 'userId'", + }); + } + const results = await req.ctx.db.collection("keys").deleteMany({ + userId: {$eq: req.body.userId}, + }); + if (results.deletedCount) { + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } }); - if (results.deletedCount) { - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", + + router.delete("/:keyId", async (req, res) => { + const results = await req.ctx.db.collection("keys").deleteOne({ + _id: {$eq: req.params.keyId}, }); - } -}); + if (results.deletedCount) { + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } + }); -export default router; + export default router; diff --git a/routes/memberProjects.js b/routes/memberProjects.js index c2589da..7ee04a4 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -16,6 +16,7 @@ const ARRAYS = new Set([ router.get("/by-key/:associationId", async (req, res) => { const key = await req.ctx.db.collection("keys").findOne({ [req.associationType]: {$eq: req.params.associationId}, + enabled: { $eq: true }, }); if (!key) { diff --git a/server.js b/server.js index 3f545ac..2af2a77 100644 --- a/server.js +++ b/server.js @@ -89,7 +89,7 @@ connectionPromise.then(async () => { ); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (req.method === "OPTIONS") return res.sendStatus(204); next(); From 5b546128331c7a48fdb013364582100a2bb62424 Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 12 Jul 2026 20:06:50 -0400 Subject: [PATCH 03/11] feat: datadog metrics --- metrics.js | 7 +++++++ package.json | 1 + server.js | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 metrics.js diff --git a/metrics.js b/metrics.js new file mode 100644 index 0000000..3ff3aae --- /dev/null +++ b/metrics.js @@ -0,0 +1,7 @@ +import StatsD from 'hot-shots'; + +export const statsd = new StatsD({ + prefix: 'gatekeeper.', + globalTags: { service: 'gatekeeper-mqtt' }, + errorHandler: (err) => console.error('StatsD error:', err), +}); diff --git a/package.json b/package.json index 46a5ab0..4ac65e9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", + "hot-shots": "^17.0.0", "jose": "^5.0.0", "ldapjs": "^2.3.1", "mongodb": "^7.1.0", diff --git a/server.js b/server.js index 3f545ac..deacc1a 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ +import { statsd } from "./metrics.js"; import express from "express"; import mqtt from "mqtt"; import { MongoClient, ObjectId } from "mongodb"; @@ -220,6 +221,7 @@ connectionPromise.then(async () => { } else if (topic.endsWith("/heartbeat")) { const doorId = topic.slice(3, -10); doorHeartbeats.set(doorId, Date.now()); + statsd.increment('door.heartbeat', { door: doorId }); console.log(`ACKing a heartbeat from ${doorId}!`); } }); From ea3e59dc4942d2ed4411a60de05792ab8d9a6434 Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 12 Jul 2026 23:22:11 -0400 Subject: [PATCH 04/11] fix: Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c388d55..cee88c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN corepack enable COPY package.json /app/package.json COPY pnpm-lock.yaml /app/pnpm-lock.yaml WORKDIR /app -RUN pnpm i --production=true +RUN pnpm i --production=true --no-optional COPY . /app CMD pnpm run kube From b372fc3ac384a199cf204254c54680749db4699e Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 03:14:27 -0400 Subject: [PATCH 05/11] feat: access route --- middleware/hybridAuth.js | 3 ++- middleware/oidc.js | 3 ++- routes/keys.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/middleware/hybridAuth.js b/middleware/hybridAuth.js index f93d69c..ff9e435 100644 --- a/middleware/hybridAuth.js +++ b/middleware/hybridAuth.js @@ -10,9 +10,10 @@ export function hybridAuth(realm) { if (authHeader.startsWith("Bearer ")) { try { - const { userId, groups } = await validateToken(authHeader.slice(7), USER_SCOPE); + const { userId, groups, username } = await validateToken(authHeader.slice(7), USER_SCOPE); req.ctx.userId = userId; req.ctx.groups = groups; + req.ctx.username = username; req.ctx.authMethod = "oidc"; next(); } catch (err) { diff --git a/middleware/oidc.js b/middleware/oidc.js index 7454834..fa83616 100644 --- a/middleware/oidc.js +++ b/middleware/oidc.js @@ -56,7 +56,8 @@ export async function validateToken(token, requiredScope = null) { } const groups = payload.groups ?? []; - return { userId, groups }; + const username = payload.preferred_username + return { userId, groups, username }; } export function oidcAuth(scope) { diff --git a/routes/keys.js b/routes/keys.js index 8b1e57d..85ef9a4 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -4,6 +4,15 @@ const router = Router(); + router.post("/access", async (req, res) => { + const { reason } = req.body; + if (typeof reason != "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing reason field" }); + } + console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + res.status(204).send(null); + }); + router.get("/by-user", async (req, res) => { if (typeof req.query.userId != "string") { return res.status(422).json({ message: "Missing 'userId' query param" }); @@ -61,6 +70,9 @@ updates[key] = req.body[key]; } } + + const owner = req.body.username ?? "not found"; + console.log(`access: ${req.ctx.username} updated ${owner}'s key ${req.params.id}`); await req.ctx.db.collection("keys").updateOne( { _id: {$eq: req.params.id}, @@ -116,10 +128,13 @@ }); router.delete("/:keyId", async (req, res) => { + const owner = req.body?.username ?? "unknown"; + const results = await req.ctx.db.collection("keys").deleteOne({ _id: {$eq: req.params.keyId}, }); if (results.deletedCount) { + console.log(`keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`); return res.status(204).send(null); } else { return res.status(404).json({ From a104ccac3b47616a81c8ee41bffefe256f67eae6 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:10:49 -0400 Subject: [PATCH 06/11] feat: audit route + uid in logEntry + username --- access.js | 6 ++++++ middleware/hybridAuth.js | 5 +++-- middleware/oidc.js | 5 +++-- routes/keys.js | 20 ++++++++++++++++++++ routes/logs.js | 37 ++++++++++++++++++++++++++++++++++++- server.js | 3 +++ 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/access.js b/access.js index 9e0f839..301d64f 100644 --- a/access.js +++ b/access.js @@ -21,3 +21,9 @@ export async function checkAccess(db, userId, doorId) { ); return groupTicket?.granted; } + +export async function recordAudit(db, entry) { + db.collection("auditLogs").insertOne({ ...entry, timestamp: new Date() }).catch((err) => { + console.error("Failed to write audit entry", err); + }); +} \ No newline at end of file diff --git a/middleware/hybridAuth.js b/middleware/hybridAuth.js index ff9e435..044b53f 100644 --- a/middleware/hybridAuth.js +++ b/middleware/hybridAuth.js @@ -10,10 +10,11 @@ export function hybridAuth(realm) { if (authHeader.startsWith("Bearer ")) { try { - const { userId, groups, username } = await validateToken(authHeader.slice(7), USER_SCOPE); + const { userId, groups, username, name } = await validateToken(authHeader.slice(7), USER_SCOPE); req.ctx.userId = userId; req.ctx.groups = groups; req.ctx.username = username; + req.ctx.name = name; req.ctx.authMethod = "oidc"; next(); } catch (err) { @@ -29,4 +30,4 @@ export function hybridAuth(realm) { next(); } }; -} +} \ No newline at end of file diff --git a/middleware/oidc.js b/middleware/oidc.js index fa83616..267fa8f 100644 --- a/middleware/oidc.js +++ b/middleware/oidc.js @@ -56,8 +56,9 @@ export async function validateToken(token, requiredScope = null) { } const groups = payload.groups ?? []; - const username = payload.preferred_username - return { userId, groups, username }; + const username = payload.preferred_username; + const name = payload.name; + return { userId, groups, username, name }; } export function oidcAuth(scope) { diff --git a/routes/keys.js b/routes/keys.js index 85ef9a4..d5f07fb 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,6 +1,7 @@ import { Router } from "express"; import crypto from "crypto"; import { REALM_NAMES } from "../constants.js"; + import { recordAudit } from "../access.js"; const router = Router(); @@ -10,6 +11,12 @@ return res.status(422).json({ message: "Missing reason field" }); } console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Keys", + reason, + }); res.status(204).send(null); }); @@ -81,6 +88,13 @@ $set: updates, } ); + const change = updates.enabled === false ? "Disabled" : updates.enabled === true ? "Enabled" : "updated"; + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Updated Keys", + reason: `${change} ${owner}'s key ${req.params.id}`, + }); res.status(204).send(null); }); @@ -135,6 +149,12 @@ }); if (results.deletedCount) { console.log(`keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`); + await recordAudit(req.ctx.db,{ + username: req.ctx.username, + name: req.ctx.name, + action: "Deleted Keys", + reason: `deleted ${owner}'s key ${req.params.keyId}`, + }); return res.status(204).send(null); } else { return res.status(404).json({ diff --git a/routes/logs.js b/routes/logs.js index f25429d..588274c 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -1,8 +1,30 @@ import { Router } from "express"; +import { recordAudit } from "../access.js"; + const router = Router(); +router.post("/access", async (req, res) => { + const { reason } = req.body; + if (typeof reason != "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing 'reason' field" }); + } + console.log(`access: ${req.ctx.username} viewed logs || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Logs", + reason, + }); + res.status(204).send(null); +}); + +router.get("/doors", async (req, res) => { + const doors = await req.ctx.db.collection("accessLogs").distinct("doorName"); + res.json(doors.filter(Boolean)); +}); + router.get("/", async (req, res) => { - const { cursor, since, until } = req.query; + const { cursor, since, until, search, door, granted } = req.query; const query = {}; if (since || until || cursor) { query.timestamp = {}; @@ -11,6 +33,19 @@ router.get("/", async (req, res) => { if (until) query.timestamp.$lte = new Date(until); } + const ands = []; + if (door && door !== "all") { + ands.push({ $or: [{ doorName: door }, { door }] }); + } + if (search) { + const re = new RegExp(search, "i"); + ands.push({ $or: [{ doorName: re }, { door: re }, { username: re }, { name: re }] }); + } + if (ands.length) query.$and = ands; + + if (granted === "granted") query.granted = true; + if (granted === "denied") query.granted = false; + const logs = await req.ctx.db .collection("accessLogs") .find(query) diff --git a/server.js b/server.js index 75a6a66..41b3121 100644 --- a/server.js +++ b/server.js @@ -19,6 +19,7 @@ import keys from "./routes/keys.js"; import users from "./routes/users.js"; import mobile from "./routes/mobile.js"; import logs from "./routes/logs.js"; +import audit from "./routes/audit.js"; //fetch user from the https endpoint async function fetchUser(endpoint, token, memberProjectsId) { @@ -127,6 +128,7 @@ connectionPromise.then(async () => { app.use("/admin/keys", hybridAuth("admin"), requireGroup("rtp"), keys); app.use("/admin/users", hybridAuth("admin"), requireGroup("rtp"), users); app.use("/admin/logs", hybridAuth("admin"), requireGroup("rtp"), logs); + app.use("/admin/audit", hybridAuth("admin"), requireGroup("rtp"), audit); app.use("/mobile", mobile); client.on("connect", async () => { @@ -199,6 +201,7 @@ connectionPromise.then(async () => { name, doorsId: payload.association, keyId: key._id, + uid: key.uid ?? null, granted: !!granted, }; From cfc75b176a03f68f2e58adb17327a508af0e7685 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:11:21 -0400 Subject: [PATCH 07/11] feat: audit route (mostly stuff from logs route but for audit logs) --- routes/audit.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 routes/audit.js diff --git a/routes/audit.js b/routes/audit.js new file mode 100644 index 0000000..fbf90ac --- /dev/null +++ b/routes/audit.js @@ -0,0 +1,34 @@ +import { Router } from "express"; +const router = Router(); + +router.get("/", async (req, res) => { + const { cursor, search, action } = req.query; + const query = {}; + + if (cursor) { + query.timestamp = { $lt: new Date(cursor) }; + } + + const ands = []; + if (action && action !== "all") ands.push({ action }); + if (search) { + const re = new RegExp(search, "i"); + ands.push({ $or: [{ username: re }, { name: re }, { reason: re }] }); + } + if (ands.length) query.$and = ands; + + const entries = await req.ctx.db + .collection("auditLogs") + .find(query) + .sort({ timestamp: -1 }) + .limit(50) + .toArray(); + + const nextCursor = entries.length === 50 + ? entries[entries.length - 1].timestamp.toISOString() + : null; + + res.json({ entries, cursor: nextCursor }); +}); + +export default router; \ No newline at end of file From 313b1849d2343d2924c6fbd8a3d7eb77370d3fc9 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:36:38 -0400 Subject: [PATCH 08/11] docker stuff --- .dockerignore | 4 ++++ Dockerfile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0c8ddf6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +*.log +.env diff --git a/Dockerfile b/Dockerfile index cee88c7..88fcc05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,4 @@ WORKDIR /app RUN pnpm i --production=true --no-optional COPY . /app -CMD pnpm run kube +CMD ["pnpm", "run", "kube"] From e0de99a675cc3075f5d9bdc9128a3d3772792e41 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:52:13 -0400 Subject: [PATCH 09/11] docs: new routes --- docs.org | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs.org b/docs.org index bced348..609e791 100644 --- a/docs.org +++ b/docs.org @@ -17,17 +17,30 @@ *** GET /admin/users/uuid-by-uid/:uid Fetch user UUID by CSH username ** /admin/keys +*** POST /admin/keys/access + State a reason to view keys +*** GET /admin/keys/by-user + Fetch keys attached to a user *** PUT /admin/keys Add a key for a user *** PATCH /admin/keys/:id Update a key +*** GET /admin/keys/by-association/:id + Fetch key by ID *** DELETE /admin/keys/by-user Delete all keys attached to a user *** DELETE /admin/keys/:keyId Delete a key ** /admin/logs +*** POST /admin/logs/access + State a reason to view logs *** GET /admin/logs Fetch paginated access logs +*** GET /admin/logs/doors + List distinct doors that appear in access logs +** /admin/audit +*** GET /admin/audit + Fetch paginated audit logs * /projects ** GET /projects/by-key/:keyId - Fetches user details from LDAP attached to the key with the given ID (from the member project realm) + Fetches user details from LDAP attached to the key with the given ID (from the member project realm) \ No newline at end of file From 7da12c8972b5c808870419044f966ce886c274f5 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Mon, 27 Jul 2026 21:30:15 -0400 Subject: [PATCH 10/11] feat: accessLog expires after 6 months --- server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server.js b/server.js index 41b3121..37f7a09 100644 --- a/server.js +++ b/server.js @@ -49,6 +49,7 @@ connectionPromise.then(async () => { db.collection("keys").createIndex("drinkId", {unique: true}), db.collection("keys").createIndex("memberProjectsId", {unique: true}), db.collection("accessLogs").createIndex({ timestamp: -1 }), + db.collection("accessLogs").createIndex({ timestamp: 1 }, { expireAfterSeconds: 15778476 }), ]); async function scheduledTasks() { From 6a6b81f8ddced98ca86baeab96317fd16cefc272 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Tue, 28 Jul 2026 03:10:07 -0400 Subject: [PATCH 11/11] feat: rejects request if door is offline --- routes/doors.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/routes/doors.js b/routes/doors.js index 820e0e7..049aa4d 100644 --- a/routes/doors.js +++ b/routes/doors.js @@ -4,20 +4,24 @@ import { checkAccess } from "../access.js"; const router = Router(); -router.get("/:doorId/status", (req, res) => { +function getDoorStatus(doorId){ // If it's been more than 1 minute, we assume something is broken... - const lastHeartbeat = doorHeartbeats.get(req.params.doorId); + const lastHeartbeat = doorHeartbeats.get(doorId); if (lastHeartbeat) { - res.json({ + return { guess: Date.now() - lastHeartbeat > 1000 * 60 ? "offline" : "online", lastHeartbeat, - }); + }; } else { - res.json({ + return { guess: "offline", lastHeartbeat: 0, - }); + }; } +} + +router.get("/:doorId/status", (req, res) => { + res.json(getDoorStatus(req.params.doorId)); }); router.get("/", async (req, res) => { @@ -46,6 +50,11 @@ router.post("/:doorId/unlock", async (req, res) => { return res.status(403).json({ message: "Access denied" }); } } + + if (getDoorStatus(req.params.doorId).guess === "offline") { + return res.status(409).json({ message: "Door is offline" }); + } + req.ctx.mqtt.publish(`gk/${req.params.doorId}/unlock`, ""); res.status(204).send(null); });