diff --git a/migrations/metadata/databases/default/tables/public_form_entries.yaml b/migrations/metadata/databases/default/tables/public_form_entries.yaml index 7496cd3..897b7f5 100644 --- a/migrations/metadata/databases/default/tables/public_form_entries.yaml +++ b/migrations/metadata/databases/default/tables/public_form_entries.yaml @@ -38,15 +38,4 @@ select_permissions: - created_at - widget_id filter: {} - role: user -event_triggers: -- definition: - enable_manual: false - insert: - columns: "*" - name: webhooks-plip-form-mautic - retry_conf: - interval_sec: 10 - num_retries: 0 - timeout_sec: 60 - webhook: http://n8n.enedina.bonde.org/webhook/plip-form-mautic + role: user \ No newline at end of file diff --git a/migrations/metadata/databases/default/tables/public_plip_signatures.yaml b/migrations/metadata/databases/default/tables/public_plip_signatures.yaml index e692df4..1f12f7f 100644 --- a/migrations/metadata/databases/default/tables/public_plip_signatures.yaml +++ b/migrations/metadata/databases/default/tables/public_plip_signatures.yaml @@ -65,4 +65,4 @@ event_triggers: interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook: http://n8n.enedina.bonde.org/webhook/plip-signatures-mautic + webhook: "{{N8N_WEBHOOK_URL}}/plip-signatures-mautic" \ No newline at end of file diff --git a/migrations/metadata/databases/default/tables/public_plips.yaml b/migrations/metadata/databases/default/tables/public_plips.yaml index f3a4304..2b6a9d1 100644 --- a/migrations/metadata/databases/default/tables/public_plips.yaml +++ b/migrations/metadata/databases/default/tables/public_plips.yaml @@ -87,4 +87,4 @@ event_triggers: interval_sec: 10 num_retries: 0 timeout_sec: 60 - webhook_from_env: WEBHOOK_PLIP_MAUTIC_URL + webhook: "{{N8N_WEBHOOK_URL}}/plip-mautic" diff --git a/packages/domains-api/src/controllers/certificates-controller.spec.ts b/packages/domains-api/src/controllers/certificates-controller.spec.ts index 7314ed2..3eebf65 100644 --- a/packages/domains-api/src/controllers/certificates-controller.spec.ts +++ b/packages/domains-api/src/controllers/certificates-controller.spec.ts @@ -1,52 +1,135 @@ -import CertificatesController, { - Request, - // Certificate, - DNSHostedZone -} from './certificates-controller'; - -test('should create certificate', () => { - // const mockReq: Request = { - // body: { - // event: { - // data: { - // new: { - // domain: "lutarnaoecrime.org", - // id: 33, - // } - // } - // } - // } - // }; - const mockReq: Request = { - body: { - event: { - data: { - new: { - domain_name: "lutarnaoecrime.org", - id: 33, - community_id: 2 +// Mock de certificates redis API (Padrão para todos os testes) +const mockCreateWildcard = jest.fn(); +const mockCreateRouters = jest.fn(); + +jest.mock('../redis-db/certificates', () => ({ + createWildcard: mockCreateWildcard, + createRouters: mockCreateRouters +})) + +import CertificatesController from './certificates-controller'; + +describe('Certificates controller', () => { + // Mock de clients externos API e Redis (Padrão para todos os testes) + const mockGraphQLClient = { + request: jest.fn() + } + + beforeEach(() => { + jest.clearAllMocks(); + }) + + describe('certificate is create', () => { + // Mock de entradas da função (Padrão para testes que criam certificado) + const dns = { + id: 4, + community_id: 1, + domain_name: 'test.org', + ns_ok: true + }; + const request = { + body: { + event: { + data: { + new: dns } } } } - }; - // const mockData = { - // dns_hosted_zones_by_pk: { - // hosted_zone: 'aaa.com', - // name_servers: 'aaaaa.com' - // } - // }; - // const mockFn = { - // request: jest.fn().mockReturnValue({ - // data: mockData, - // errors: {} - // }) - // }; - const mRes = { status: jest.fn().mockReturnThis(), json: jest.fn(), body: mockReq.body }; - - const certificatesController = new CertificatesController(jest.fn(), jest.fn()); - certificatesController.create(mockReq, mRes); - - // expect(mRes.status).toBeCalled(); - expect(mRes.body).toBe(mockReq.body); -}); + const mockJson = jest.fn(); + const response: any = { + status: jest.fn().mockImplementation(() => ({ + json: mockJson + })) + } + + it('should return graphql response to action', async () => { + const result = { + id: 3, + dns_hosted_zone_id: request.body.event.data.new.id, + is_active: false, + community_id: request.body.event.data.new.community_id, + domain: 'test.org', + } + + // Mock de clients externos API e Redis + mockGraphQLClient.request.mockResolvedValueOnce({ mobilizations: [] }); + mockGraphQLClient.request.mockResolvedValueOnce({ insert_certificates_one: result }); + + const certificatesController = new CertificatesController(mockGraphQLClient); + await certificatesController.create(request, response); + + expect(response.status.mock.calls[0][0]).toBe(200); + expect(mockJson.mock.calls[0][0]).toBe(result); + }); + + it('should create traefik router with wildcard in redis', async () => { + mockGraphQLClient.request.mockResolvedValueOnce({ mobilizations: [] }); + + const certificatesController = new CertificatesController(mockGraphQLClient); + await certificatesController.create(request, response); + + const tRouterName = `${request.body.event.data.new.id}-${request.body.event.data.new.domain_name.replace('.', '-')}`; + + expect(mockCreateWildcard.mock.calls[0]).toEqual([tRouterName, request.body.event.data.new.domain_name]) + }); + + it('should call fetchMobilizationsByDomain', async () => { + const certificatesController = new CertificatesController(mockGraphQLClient); + await certificatesController.create(request, response); + + expect(mockGraphQLClient.request.mock.calls[0][0].variables).toEqual({ + domainName: `%${dns.domain_name}%` + }); + }); + + it('should create traefik routers for subdomains in redis', async () => { + const mobilizations = [ + { id: 1, community_id: 2, custom_domain: `www.campaign0.${dns.domain_name}` }, + { id: 2, community_id: 2, custom_domain: `www.campaign1.${dns.domain_name}` } + ] + + mockGraphQLClient.request.mockResolvedValue({ mobilizations }); + + const certificatesController = new CertificatesController(mockGraphQLClient); + await certificatesController.create(request, response); + const routerName = `${request.body.event.data.new.id}-${request.body.event.data.new.domain_name.replace('.', '-')}-www`; + + expect(mockCreateRouters.mock.calls[0]) + .toEqual([routerName, mobilizations.map(m => m.custom_domain)]) + }); + }); + + describe('certificate is not create', () => { + // Mock de entradas da função (Padrão para testes que não criam certificado) + const request = { + body: { + event: { + data: { + new: { + id: 4, + community_id: 1, + domain_name: 'test.org', + ns_ok: false + } + } + } + } + } + const mockJson = jest.fn(); + const response: any = { + status: jest.fn().mockImplementation(() => ({ + json: mockJson + })) + } + + it('should return 400 when dns is not ok', async () => { + const certificatesController = new CertificatesController(mockGraphQLClient); + await certificatesController.create(request, response); + + expect(response.status.mock.calls[0][0]).toEqual(402); + expect(mockJson.mock.calls[0][0]).toEqual({ message: 'Certificate not created because ns_ok is false.' }); + }); + }); +}) + diff --git a/packages/domains-api/src/controllers/certificates-controller.ts b/packages/domains-api/src/controllers/certificates-controller.ts index 491f1de..d2c38e8 100644 --- a/packages/domains-api/src/controllers/certificates-controller.ts +++ b/packages/domains-api/src/controllers/certificates-controller.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import logger from '../config/logger'; import { gql } from '../graphql-api/client'; +import { createRouters, createWildcard } from '../redis-db/certificates'; import { validationResult, check } from 'express-validator'; import sslChecker from "ssl-checker"; @@ -14,15 +15,26 @@ export interface Request { } } -export interface Certificate { +export interface CertificateTLS { id: number; + dns_hosted_zone_id: number; + is_active: boolean; + community_id: number; domain: string; + ssl_checker_response?: any; } export interface DNSHostedZone { id: number; community_id: number; domain_name: string; + ns_ok?: boolean; +} + +export interface Mobilization { + id: number; + custom_domain: string; + community_id: number; } const insert_certificate = gql`mutation ($input: certificates_insert_input!) { @@ -32,6 +44,7 @@ const insert_certificate = gql`mutation ($input: certificates_insert_input!) { is_active community_id domain + ssl_checker_response } } `; @@ -49,48 +62,56 @@ mutation ($id: Int!, $ssl_checker_response: jsonb) { } `; +export const fetch_mobilizations_by_domain = gql` + query ($domainName: String) { + mobilizations (where:{ custom_domain:{ _ilike: $domainName } }) { + id + custom_domain + community_id + } + } +`; + class CertificatesController { - private redisClient: any private graphqlClient: any - constructor(redisClient, graphqlClient) { - this.redisClient = redisClient; + constructor(graphqlClient) { this.graphqlClient = graphqlClient; } create = async (req: Request, res) => { await check('event').isObject().run(req); - // await check('password').isLength({ min: 6 }).run(req); const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } - try { - await this.insertCertificateRedis(req.body.event.data.new); - res.status(200).json(await this.insertCertificateGraphql(req.body.event.data.new)); - } catch (e: any) { - logger.info(e) - res.status(500).json({ ok: false, ...e }); + + const dns_hosted_zone = req.body.event.data.new; + + if (dns_hosted_zone.ns_ok) { + try { + const domains: string[] = await this.fetchCustomDomains(dns_hosted_zone.domain_name); + await this.insertCertificateRedis(dns_hosted_zone, domains); + res.status(200).json(await this.insertCertificateGraphql(dns_hosted_zone)); + } catch (e: any) { + logger.info(e) + res.status(500).json({ ok: false, ...e }); + } + } else { + res.status(402).json({ message: 'Certificate not created because ns_ok is false.' }); } } - private insertCertificateRedis = async (input: any) => { + private insertCertificateRedis = async (input: DNSHostedZone, domains: string[]) => { const { domain_name, id: dns_hosted_zone_id } = input; const tRouterName = `${dns_hosted_zone_id}-${domain_name.replace('.', '-')}` logger.info(`In controller - createCertificate ${tRouterName}`); - - await this.redisClient.connect(); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls`, 'true'); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/certresolver`, 'myresolver'); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/rule`, `HostRegexp(\`${domain_name}\`, \`{subdomain:.+}.${domain_name}\`)`); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/domains/0/main`, domain_name); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/tls/domains/0/sans/0`, `*.${domain_name}`); - await this.redisClient.set(`traefik/http/routers/${tRouterName}/service`, 'public@docker'); - // console.log(await this.redisClient.get('traefik')); - await this.redisClient.quit(); + + await createWildcard(tRouterName, domain_name); + await createRouters(`${tRouterName}-www`, domains); } - private insertCertificateGraphql = async (input: any) => { + private insertCertificateGraphql = async (input: any): Promise => { const { domain_name: domain, community_id, id: dns_hosted_zone_id } = input; const data: any = await this.graphqlClient.request({ document: insert_certificate, @@ -113,7 +134,18 @@ class CertificatesController { return data.update_certificates_by_pk; } - check = async (req: Request, res) => { + private fetchCustomDomains = async (domain: string): Promise => { + const data: { mobilizations: Mobilization[] } = await this.graphqlClient.request({ + document: fetch_mobilizations_by_domain, + variables: { domainName: `%${domain}%` } + }); + + logger.child({ data }).info('fetch_mobilizations_by_domain'); + + return data.mobilizations.map((mob) => mob.custom_domain); + } + + check = async (req: Request, res) => { /** * Esse evento deve ser chamado sempre que criar um novo certificado * Hasura irá fazer uma nova chamada em caso de erro no intervalo de 6 minutos diff --git a/packages/domains-api/src/redis-db/certificates.spec.ts b/packages/domains-api/src/redis-db/certificates.spec.ts new file mode 100644 index 0000000..0b9a70d --- /dev/null +++ b/packages/domains-api/src/redis-db/certificates.spec.ts @@ -0,0 +1,105 @@ +// 1. Criar um rota wildcard para o dominio principal subdomain / domain - Ex.: *.meurio.org.br meurio.org.br, auxiliomoradia.meurio.org.br +// +// 2. Criar uma rota para cada X subdominios com www - Ex.: www.auxiliomoradia.meurio.org.br, www.forapaes.meurio.org.br +// X = 100 + +const mockConnect = jest.fn(); +const mockSet = jest.fn(); +const mockQuit = jest.fn(); + +jest.mock('./client', () => ({ + connect: mockConnect, + set: mockSet, + quit: mockQuit +})) + +import { DNSHostedZone } from "../controllers/certificates-controller"; +import { createWildcard, createRouters } from "./certificates"; + +describe('Certificates Redis', () => { + const dns: DNSHostedZone = { + id: 1, + community_id: 75, + domain_name: 'nossas.link' + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createWildcard is successfully', () => { + const routerName = `${dns.id}-${dns.domain_name.replace('.', '-')}`; + + it('should open and quit connect redis', async () => { + await createWildcard(routerName, dns.domain_name); + + expect(mockConnect.mock.calls.length).toEqual(1); + expect(mockQuit.mock.calls.length).toEqual(1); + }); + + it('should register wildcard in redis', async () => { + await createWildcard(routerName, dns.domain_name); + + expect(mockSet.mock.calls[0]).toEqual([`traefik/http/routers/${routerName}/tls`, 'true']); + expect(mockSet.mock.calls[1]).toEqual([`traefik/http/routers/${routerName}/tls/certresolver`, 'myresolver']); + expect(mockSet.mock.calls[2]).toEqual([`traefik/http/routers/${routerName}/rule`, `HostRegexp(\`${dns.domain_name}\`, \`{subdomain:.+}.${dns.domain_name}\`)`]); + expect(mockSet.mock.calls[3]).toEqual([`traefik/http/routers/${routerName}/tls/domains/0/main`, dns.domain_name]); + expect(mockSet.mock.calls[4]).toEqual([`traefik/http/routers/${routerName}/tls/domains/0/sans/0`, `*.${dns.domain_name}`]); + expect(mockSet.mock.calls[5]).toEqual([`traefik/http/routers/${routerName}/service`, 'public@docker']); + }); + }); + + describe('createRouters is successfully', () => { + const routerName = `${dns.id}-${dns.domain_name.replace('.', '-')}-www`; + const domainNames = [ + `www.campanha1.${dns.domain_name}`, + `www.campanha2.${dns.domain_name}`, + `www.campanha3.${dns.domain_name}` + ] + + it('should open and quit connect redis', async () => { + await createRouters(routerName, domainNames); + + expect(mockConnect.mock.calls.length).toEqual(1); + expect(mockQuit.mock.calls.length).toEqual(1); + }); + + it('should register in redis a router with domainNames', async () => { + await createRouters(routerName, domainNames); + + expect(mockSet.mock.calls[0]).toEqual([`traefik/http/routers/${routerName}-0/tls`, 'true']); + expect(mockSet.mock.calls[1]).toEqual([`traefik/http/routers/${routerName}-0/tls/certresolver`, 'myresolver']); + expect(mockSet.mock.calls[2]).toEqual([`traefik/http/routers/${routerName}-0/service`, 'public@docker']); + expect(mockSet.mock.calls[3]).toEqual([ + `traefik/http/routers/${routerName}-0/rule`, + `Host(${domainNames.map((domain) => `\`${domain}\``).join(',')})` + ]); + }); + + it('should register in redis a router every 100 domainNames', async () => { + const manyDomainNames = Array.from({ length: 150 }, (_, index: number) => `www.campanha${index}.${dns.domain_name}`); + + await createRouters(routerName, manyDomainNames); + + console.log(mockSet.mock.calls); + expect(mockSet.mock.calls[0]).toEqual([`traefik/http/routers/${routerName}-0/tls`, 'true']); + expect(mockSet.mock.calls[1]).toEqual([`traefik/http/routers/${routerName}-1/tls`, 'true']); + + expect(mockSet.mock.calls[2]).toEqual([`traefik/http/routers/${routerName}-0/tls/certresolver`, 'myresolver']); + expect(mockSet.mock.calls[3]).toEqual([`traefik/http/routers/${routerName}-1/tls/certresolver`, 'myresolver']); + + expect(mockSet.mock.calls[4]).toEqual([`traefik/http/routers/${routerName}-0/service`, 'public@docker']); + expect(mockSet.mock.calls[5]).toEqual([`traefik/http/routers/${routerName}-1/service`, 'public@docker']); + + expect(mockSet.mock.calls[6]).toEqual([ + `traefik/http/routers/${routerName}-0/rule`, + `Host(${manyDomainNames.slice(0, 100).map((domain) => `\`${domain}\``).join(',')})` + ]); + expect(mockSet.mock.calls[7]).toEqual([ + `traefik/http/routers/${routerName}-1/rule`, + `Host(${manyDomainNames.slice(100, 150).map((domain) => `\`${domain}\``).join(',')})` + ]); + + }); + }) +}); \ No newline at end of file diff --git a/packages/domains-api/src/redis-db/certificates.ts b/packages/domains-api/src/redis-db/certificates.ts new file mode 100644 index 0000000..4bb16cf --- /dev/null +++ b/packages/domains-api/src/redis-db/certificates.ts @@ -0,0 +1,37 @@ +import redisClient from './client'; + +export const createWildcard = async (routerName: string, domainName: string): Promise => { + // Open connection with redis + await redisClient.connect(); + // Configure routers + await redisClient.set(`traefik/http/routers/${routerName}/tls`, 'true'); + await redisClient.set(`traefik/http/routers/${routerName}/tls/certresolver`, 'myresolver'); + await redisClient.set(`traefik/http/routers/${routerName}/rule`, `HostRegexp(\`${domainName}\`, \`{subdomain:.+}.${domainName}\`)`); + await redisClient.set(`traefik/http/routers/${routerName}/tls/domains/0/main`, domainName); + await redisClient.set(`traefik/http/routers/${routerName}/tls/domains/0/sans/0`, `*.${domainName}`); + await redisClient.set(`traefik/http/routers/${routerName}/service`, 'public@docker'); + // Close connection with redis + await redisClient.quit(); +} + +export const createRouters = async (routerName: string, domainNames: string[]): Promise => { + // Open connection with redis + await redisClient.connect(); + // Configure routers + let cursor = Math.floor(domainNames.length / 100); + if (domainNames.length % 100 !== 0) cursor += 1; + + + await Promise.all(Array.from({ length: cursor > 0 ? cursor : 1 }, async (_, index: number) => { + await redisClient.set(`traefik/http/routers/${routerName}-${index}/tls`, 'true'); + await redisClient.set(`traefik/http/routers/${routerName}-${index}/tls/certresolver`, 'myresolver'); + await redisClient.set(`traefik/http/routers/${routerName}-${index}/service`, 'public@docker'); + await redisClient.set( + `traefik/http/routers/${routerName}-${index}/rule`, + `Host(${domainNames.slice(index * 100, (index + 1) * 100).map((domain) => `\`${domain}\``).join(',')})` + ); + })) + + // Close connection with redis + await redisClient.quit(); +} \ No newline at end of file diff --git a/packages/domains-api/src/routes/events-route.ts b/packages/domains-api/src/routes/events-route.ts index 576c8ad..e57508c 100644 --- a/packages/domains-api/src/routes/events-route.ts +++ b/packages/domains-api/src/routes/events-route.ts @@ -1,12 +1,12 @@ // import dependencies and initialize the express router import express from 'express'; import CertificatesController from '../controllers/certificates-controller'; -import redisClient from '../redis-db/client' +// import redisClient from '../redis-db/client' import { client } from '../graphql-api/client'; const router = express.Router(); -const certificatesController = new CertificatesController(redisClient, client); +const certificatesController = new CertificatesController(client); // define routes router.post('/create-certificate', certificatesController.create);