HMAC y gestion de subscripciones

This commit is contained in:
2025-12-26 13:20:06 +01:00
parent 6f48b6f518
commit a2c017d67f
5 changed files with 107 additions and 26 deletions

View File

@@ -1 +1 @@
{"version":"4.0.16","results":[[":src/controllers/subscriptions.test.ts",{"duration":8.344391000000002,"failed":false}]]} {"version":"4.0.16","results":[[":src/controllers/subscriptions.test.ts",{"duration":7.715925999999996,"failed":false}]]}

View File

@@ -1,12 +1,19 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { SubscriptionManager } from './subscriptions';
export const handleOrdersWebhook = async (req: Request, res: Response) => {
try { export function ordersHandlerBuilder(subscriptionManager: SubscriptionManager) {
const event = req.body; const subsManager = subscriptionManager
console.log('Received webhook event:', JSON.stringify(event, null, 2)); return async function (req: Request, res: Response) {
res.status(200).json({ received: true }); try {
} catch (error) { const event = req.body;
console.error('Error processing webhook:', error); console.log('Received webhook event:', JSON.stringify(event, null, 2));
res.status(500).json({ error: 'Internal Server Error' }); subsManager.poll(event)
} res.status(200).json({ received: true });
}; } catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
};
}

View File

@@ -2,19 +2,24 @@
* Simulacion de que clientes estan subscritos a que eventos * Simulacion de que clientes estan subscritos a que eventos
* */ * */
import { error } from "node:console"
import { Shopify } from "../data/webhooks/order" import { Shopify } from "../data/webhooks/order"
import http from "node:http" import http from "node:http"
import { Request, Response } from 'express';
import { generateHMACSignature } from "#middleware/hmac.js"
import { config, webhooksConfig } from "#config/index.js";
export type SubscriptionData = { export type SubscriptionData = {
topic: string, topic: string,
subscriptors: { subscriptors: SubscriptorData[]
host: string, }
port: string,
endpoint: string, export type SubscriptorData = {
method: "POST" | "GET" | "PUT" | "DELETE" host: string,
open: Date, port: string,
}[] endpoint: string,
secretkey: string,
method: "POST" | "GET" | "PUT" | "DELETE"
open: Date,
} }
export type ShopifyEvent = { export type ShopifyEvent = {
@@ -27,6 +32,38 @@ type Topic = string
/** Mapa topic -> subscriber */ /** Mapa topic -> subscriber */
export const subscribers = new Map<Topic, SubscriptionData>() export const subscribers = new Map<Topic, SubscriptionData>()
export function subscriptonHandlerBuilder(subscriptionManager: SubscriptionManager) {
const subsManager = subscriptionManager
return function (req: Request, res: Response) {
try {
const event = req.body;
const secretkey = req.body.secretkey || webhooksConfig.apiSecret || 1234
const subscriptorData: SubscriptorData = {
host: event.host,
endpoint: event.endpoint,
method: event.method,
open: new Date(),
port: event.port,
secretkey: secretkey // Es solo para mock no usar en prod
}
const topic = event.topic
subsManager.addSubscriber({
topic: topic,
subscriber: subscriptorData
})
console.log('Received webhook subscriber:',
JSON.stringify(event, null, 2));
res.status(200).json({ received: true, key: secretkey });
} catch (error) {
console.error('Error processing webhook:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}
}
export class SubscriptionManager { export class SubscriptionManager {
private subscriptions: Map<Topic, SubscriptionData> = new Map<Topic, SubscriptionData>() private subscriptions: Map<Topic, SubscriptionData> = new Map<Topic, SubscriptionData>()
@@ -36,6 +73,18 @@ export class SubscriptionManager {
} }
} }
public addSubscriber(args: { topic: string, subscriber: SubscriptorData }) {
const topic = args.topic
if (topic == undefined) throw new Error("Topic vacio")
const topicSubscribers = this.subscriptions.get(topic)
if (topicSubscribers == undefined) {
this.subscriptions.set(topic, {
topic,
subscriptors: [args.subscriber]
})
}
}
public poll(event: ShopifyEvent) { public poll(event: ShopifyEvent) {
const topic = event.topic const topic = event.topic
@@ -47,25 +96,33 @@ export class SubscriptionManager {
const subscriptors = this.subscriptions.get(topic) const subscriptors = this.subscriptions.get(topic)
for (const sub of subscriptors!.subscriptors) { for (const sub of subscriptors!.subscriptors) {
const body = {
id: 1234
}
const parsedBody = JSON.stringify(body)
const signature = generateHMACSignature(parsedBody, sub.secretkey)
const request = requestBuilder({ const request = requestBuilder({
method: sub.method, method: sub.method,
headers: shopifyHeaderBuilder({ headers: shopifyHeaderBuilder({
topic: topic, topic: topic,
signature: "1234" signature: signature
}), }),
host: sub.host, host: sub.host,
port: sub.port, port: sub.port,
endpoint: sub.endpoint endpoint: sub.endpoint
}) })
request.write(parsedBody)
// Data puede venir en chunks! // Data puede venir en chunks!
request.on("data", () => console.log) request.on("data", () => console.log)
request.on("end", () => console.log) request.on("end", () => console.log)
request.on("error", () => console.error) request.on("error", () => console.error)
request.end()
} }
} }
} }
// TODO: Crear una fucnion especifica para los request con HMAC
function requestBuilder(args: { function requestBuilder(args: {
method: string, method: string,
headers: Object, headers: Object,
@@ -82,7 +139,6 @@ function requestBuilder(args: {
Object.entries(args.headers).forEach(([name, value]) => { Object.entries(args.headers).forEach(([name, value]) => {
request.setHeader(name, String(value)) request.setHeader(name, String(value))
}) })
return request return request
} }
@@ -93,6 +149,9 @@ function headerBuilder(args: {}) {
} }
} }
/**
* TODO: Estoy confiando que la firma esté ok
*/
function shopifyHeaderBuilder(args: { function shopifyHeaderBuilder(args: {
topic: string, topic: string,
signature: string signature: string

11
src/middleware/hmac.ts Normal file
View File

@@ -0,0 +1,11 @@
import crypto from "crypto"
//HMAC ES SIMETRICO
//El cliente y el server acuerdan la clave
export function generateHMACSignature(msg: string, key: string) {
return crypto.createHmac("sha256", key)
.update(msg)
.digest("base64")
}

View File

@@ -1,12 +1,16 @@
import { Router } from 'express'; import { Router } from 'express';
import { handleWebhook } from '../controllers/webhook'; import { ordersHandlerBuilder } from '#controllers/orders.webhook.js';
import { verifyWebhookSignature } from '../middleware/verifySig'; import { SubscriptionManager, subscriptonHandlerBuilder } from '#controllers/subscriptions.js';
const webhookRouter = Router(); const webhookRouter = Router();
const subscriptions = new SubscriptionManager()
// subto // subto
webhookRouter.post('/subto', verifyWebhookSignature, handleWebhook); webhookRouter.post('/subto', subscriptonHandlerBuilder(subscriptions));
// Simulacion de los webhook de shopify // Simulacion de los webhook de shopify
webhookRouter.post('/shopify/orders', verifyWebhookSignature, handleWebhook); // Al llamar se supone que se genera el evento
webhookRouter.post('/shopify/orders', ordersHandlerBuilder(subscriptions));
export default webhookRouter; export default webhookRouter;