HMAC y gestion de subscripciones
This commit is contained in:
2
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
2
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
@@ -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}]]}
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
11
src/middleware/hmac.ts
Normal 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")
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user