aboutsummaryrefslogtreecommitdiffstats
path: root/lib/sockets.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sockets.ts')
-rw-r--r--lib/sockets.ts27
1 files changed, 25 insertions, 2 deletions
diff --git a/lib/sockets.ts b/lib/sockets.ts
index 028551ed..610d38fd 100644
--- a/lib/sockets.ts
+++ b/lib/sockets.ts
@@ -1,9 +1,21 @@
+import config from "$config";
import store from "$lib/store";
import jwt from "jsonwebtoken";
import { v4 } from "uuid";
import { err, warn } from "$lib/logging";
import { fail, getUser } from "$lib/utils";
+// Verify the token's signature — never trust jwt.decode() here. A decode-only
+// gate let anyone forge {id: <victim uid>} (alg:none or any key) and register
+// their socket under the victim's uid, receiving every emit for that account.
+const verifyToken = (token) => {
+ try {
+ return jwt.verify(token, config.jwt);
+ } catch (e) {
+ return null;
+ }
+};
+
const code = 1000;
const all = {};
const subscriptions = [];
@@ -23,6 +35,9 @@ export const emit = (uid, type, data) => {
if (!store.sockets[uid]) return;
for (const id in store.sockets[uid]) {
const ws = store.sockets[uid][id];
+ // Read-only sockets (POS terminals logged in with a "-ro" token) only
+ // get payment notifications, mirroring the HTTP read-only whitelist.
+ if (ws.readonly && type !== "payment") continue;
ws.send(JSON.stringify({ type, data }));
}
};
@@ -44,9 +59,16 @@ export const broadcast = (type, data) => {
const track = async (ws, token) => {
const { id } = ws;
- const { id: uid } = jwt.decode(token);
+ let { id: uid } = verifyToken(token) || {};
if (!uid) fail("Invalid JWT token");
+
+ // Read-only tokens (see users.ro) carry "<uid>-ro". The HTTP auth layer
+ // strips the suffix for whitelisted routes; do the same here so a POS
+ // device can log in and receive payment events, but nothing else.
+ const readonly = uid.endsWith("-ro");
+ if (readonly) uid = uid.slice(0, -3);
+
const user = await getUser(uid);
if (!user) fail(`User not found ${uid}`);
@@ -61,6 +83,7 @@ const track = async (ws, token) => {
store.sockets[uid][id] = ws;
users[id] = uid;
ws.user = user;
+ ws.readonly = readonly;
};
Bun.serve({
@@ -97,7 +120,7 @@ Bun.serve({
case "login":
try {
- if (!data || data === "null" || !jwt.decode(data))
+ if (!data || data === "null" || !verifyToken(data))
return ws.close(code, `bad token ${data}`);
await track(ws, data);