Skip to content

Node.js: Gateway — ACL + Dedup

Bu sayfada ACL (I₁) ve Dedup / İdempotency (I₂) Node.js ile nasıl uygulanır gösterilir.


Gereksinimler

bash
npm install ws nats redis jsonwebtoken
  • Redis: Dedup set (ve isteğe bağlı ACL için key'ler).
  • JWT: CONNECT/setup'ta token doğrulama.

ACL Store (Bellek + Redis Örneği)

javascript
const redis = require('redis');
const redisClient = redis.createClient({ url: process.env.REDIS_URL || 'redis://localhost:6379' });

async function aclAllow(userId, topic, action) {
  const key = `acl:${userId}:${action}`;
  const allowed = await redisClient.sMembers(key);
  const exact = allowed.includes(topic);
  const wildcard = allowed.some((t) => t.endsWith('*') && topic.startsWith(t.slice(0, -1)));
  return exact || wildcard;
}

// Örnek veri (uygulama başlangıcında veya admin API ile):
// await redisClient.sAdd('acl:user1:read', 'risk.alerts', 'prices.*');
// await redisClient.sAdd('acl:user1:write', 'orders');

Dedup: Redis Set ile

javascript
const DEDUP_TTL = 3600; // 1 saat

async function isDuplicate(userId, messageId) {
  const key = `dedup:${userId}`;
  const added = await redisClient.sAdd(key, messageId);
  if (added === 0) return true;
  await redisClient.expire(key, DEDUP_TTL);
  return false;
}

Gateway Handler: SUBSCRIBE + ACL

javascript
async function handleSubscribe(ws, msg) {
  const { topic } = msg;
  if (!ws.userId) {
    ws.send(JSON.stringify({ type: 'error', code: 'AUTH_REQUIRED' }));
    return;
  }
  const allowed = await aclAllow(ws.userId, topic, 'read');
  if (!allowed) {
    ws.send(JSON.stringify({ type: 'error', code: 'ACL_DENIED', topic }));
    return;
  }
  addSubscriber(ws, topic);
  if (!ws.topics) ws.topics = new Set();
  ws.topics.add(topic);
  ws.send(JSON.stringify({ type: 'subscribed', topic }));
}

Gateway Handler: PUBLISH + ACL + Dedup

javascript
async function handlePublish(ws, msg) {
  const { topic, messageId, payload } = msg;
  if (!ws.userId) {
    ws.send(JSON.stringify({ type: 'error', code: 'AUTH_REQUIRED' }));
    return;
  }
  const allowed = await aclAllow(ws.userId, topic, 'write');
  if (!allowed) {
    ws.send(JSON.stringify({ type: 'error', code: 'ACL_DENIED', topic }));
    return;
  }
  const dup = await isDuplicate(ws.userId, messageId);
  if (dup) {
    ws.send(JSON.stringify({ type: 'ack', messageId, status: 'duplicate' }));
    return;
  }
  const seqNo = await nextSeqNo(topic);
  const payloadWithSeq = { ...payload, seqNo, messageId };
  nc.publish(topic, Buffer.from(JSON.stringify(payloadWithSeq)));
  ws.send(JSON.stringify({ type: 'ack', messageId, status: 'ok' }));
}

SeqNo: Redis INCR

javascript
async function nextSeqNo(topic) {
  const key = `seq:${topic}`;
  return await redisClient.incr(key);
}

Ana Mesaj Yönlendirme

javascript
ws.on('message', async (data) => {
  try {
    const msg = JSON.parse(data.toString());
    if (msg.type === 'setup' || msg.type === 'connect') {
      ws.userId = getUserIdFromToken(msg.token);
      if (!ws.userId) {
        ws.send(JSON.stringify({ type: 'error', code: 'AUTH_FAILED' }));
        return;
      }
      ws.send(JSON.stringify({ type: 'ready' }));
      return;
    }
    if (msg.type === 'subscribe') {
      await handleSubscribe(ws, msg);
      return;
    }
    if (msg.type === 'publish') {
      await handlePublish(ws, msg);
      return;
    }
  } catch (err) {
    ws.send(JSON.stringify({ type: 'error', message: err.message }));
  }
});

Akış Özeti (Mermaid)

Tam çalışan örnek: Tam E2E Örnek.

Star the repo on GitHub if this documentation is useful — link in the navbar above.