From baa7284a8e4e80e101988f2c57fe891a463bf885 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Tue, 3 Dec 2024 11:59:46 +0000 Subject: [PATCH 01/45] chore(integration-templates): Automated commit updating flows.yaml based on changes in https://github.com/NangoHQ/integration-templates/commit/cd7fcfb034ccb38b450c3c0b235a01c650bc2585 by Khaliq. Commit message: fix(lever-ashby) yaml cleanup (#131) --- packages/shared/flows.yaml | 383 ++++++++++++++++++++++++------------- 1 file changed, 252 insertions(+), 131 deletions(-) diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index 0fd5751c6d..c7248cf81e 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -526,6 +526,7 @@ integrations: endpoint: method: GET path: /candidates + group: Candidates jobs: runs: every hour output: AshbyJob @@ -536,6 +537,7 @@ integrations: endpoint: method: GET path: /jobs + group: Jobs actions: create-application: output: AshbyCreateApplicationResponse @@ -545,6 +547,7 @@ integrations: endpoint: method: POST path: /applications + group: Applications create-note: output: AshbyCreateNoteResponse input: AshbyCreateNoteInput @@ -553,6 +556,7 @@ integrations: endpoint: method: POST path: /notes + group: Notes application-change-source: output: AshbyResponse input: ChangeSource @@ -579,13 +583,14 @@ integrations: output: AshbyResponse input: ChangeSource | ChangeStage | UpdateHistory description: | - Action to update an application stage. + Action to update an application. endpoint: method: PATCH - path: /applications/update + path: /applications group: Applications scopes: - candidatesWrite + version: 1.0.0 application-update-history: output: AshbyResponse input: UpdateHistory @@ -601,7 +606,7 @@ integrations: output: AshbyResponse input: CreateCandidate description: | - Action to update history an application stage. + Action to create a candidate. endpoint: method: POST path: /candidates @@ -5751,9 +5756,11 @@ integrations: input: LeverCreateNoteInput endpoint: method: POST - path: /lever/create-note + path: /notes + group: Notes scopes: - notes:write:admin + version: 1.0.0 create-opportunity: description: | Action to create candidates and opportunities in Lever @@ -5768,16 +5775,19 @@ integrations: version: 1.0.0 get-stages: description: | - Action to get lists all pipeline stages in your Lever account. + Action to get lists all pipeline stages. Note that this does + not paginate the response so it is possible that not all stages + are returned. output: GetStages endpoint: method: GET - path: /stages - group: Opportunities + path: /stages/limited + group: Stages + version: 1.0.0 users: description: > Lists all the users in your Lever account. Only active users are - included by default.. + included by default. output: SuccessResponse endpoint: method: GET @@ -5785,29 +5795,33 @@ integrations: group: Users get-postings: description: | - Get all Posts for your account in Lever + Get all posts for your account. Note that this does + not paginate the response so it is possible that not all postings + are returned. output: SuccessResponse endpoint: method: GET - path: /posts/single + path: /posts/limited group: Posts + version: 1.0.0 get-archive-reasons: description: | - Get all Posts for your account in Lever + Get all archived reasons output: SuccessResponse endpoint: method: GET path: /archived/reasons - group: Opportunities - posting: + group: Archived + get-posting: description: | - Get single posts for your account in Lever + Get single post for your account in Lever output: SuccessResponse input: SinglePost endpoint: method: GET - path: /posts + path: /posts/single group: Posts + version: 1.0.0 update-opportunity-links: description: | Update the links in an opportunity @@ -5815,8 +5829,9 @@ integrations: input: UpdateLinks endpoint: method: POST - path: /links + path: /opportunities/links group: Opportunities + version: 1.0.0 update-opportunity-sources: description: | Update the sources in an opportunity @@ -5824,8 +5839,9 @@ integrations: input: UpdateSources endpoint: method: POST - path: /sources + path: /opportunities/sources group: Opportunities + version: 1.0.0 update-opportunity-stage: description: | Update the stage in an opportunity @@ -5833,8 +5849,9 @@ integrations: input: UpdateOpportunityStage endpoint: method: POST - path: /stages + path: /opportunities/stages group: Opportunities + version: 1.0.0 update-opportunity-tags: description: | Update the tags in an opportunity @@ -5842,8 +5859,9 @@ integrations: input: UpdateTags endpoint: method: POST - path: /tags + path: /opportunities/tags group: Opportunities + version: 1.0.0 update-opportunity-archived: description: | Update the tags in an opportunity @@ -5851,8 +5869,9 @@ integrations: input: ArchiveOpportunity endpoint: method: PUT - path: /archived + path: /opportunities/archived group: Opportunities + version: 1.0.0 apply-posting: description: > Submit an application on behalf of a candidate. This endpoint can only @@ -5893,54 +5912,63 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/opportunities-applications + path: /applications + group: Applications scopes: - applications:read:admin + version: 1.0.0 opportunities-feedbacks: runs: every 6 hours description: > - Fetches a list of all feedback forms for a candidate for a specific - Opportunity in lever + Fetches a list of all feedback forms for a candidate for every single + opportunity output: LeverOpportunityFeedback sync_type: full endpoint: method: GET - path: /lever/opportunities-feedbacks + path: /opportunities/feedback + group: Opportunities scopes: - feedback:read:admin opportunities-interviews: runs: every 6 hours description: | - Fetches a list of all interviewers for a specific Opportunity in lever + Fetches a list of all interviews for every single opportunity output: LeverOpportunityInterview sync_type: full endpoint: method: GET - path: /lever/opportunities-interviews + path: /opportunities/interviewers + group: Opportunities scopes: - interviews:read:admin + version: 1.0.0 opportunities-notes: runs: every 6 hours description: | - Fetches a list of all notes for a specific candidate in lever + Fetches a list of all notes for every single opportunity output: LeverOpportunityNote sync_type: full endpoint: method: GET - path: /lever/opportunities-notes + path: /opportunities-notes + group: Notes scopes: - notes:read:admin + version: 1.0.0 opportunities-offers: runs: every 6 hours description: | - Fetches a list of all offers for a specific candidate in lever. + Fetches a list of all offers for every single opportunity output: LeverOpportunityOffer sync_type: full endpoint: method: GET - path: /lever/opportunities-offers + path: /opportunities/offers + group: Offers scopes: - offers:write:admin + version: 1.0.0 postings: runs: every 6 hours description: | @@ -5949,10 +5977,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings + path: /postings + group: Postings scopes: - postings:read:admin - postings-apply: + version: 1.0.0 + postings-questions: runs: every 6 hours description: > Fetches a list of all questions included in a posting’s application @@ -5961,10 +5991,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings-apply + path: /postings/questions + group: Postings scopes: - postings:read:admin - stage: + version: 1.0.0 + stages: runs: every 6 hours description: | Fetches a list of all pipeline stages in Lever @@ -5972,9 +6004,11 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/stage + path: /stages + group: Stages scopes: - stages:read:admin + version: 1.0.0 models: LeverOpportunity: id: string @@ -6358,9 +6392,11 @@ integrations: input: LeverCreateNoteInput endpoint: method: POST - path: /lever/create-note + path: /notes + group: Notes scopes: - notes:write:admin + version: 1.0.0 create-opportunity: description: | Action to create candidates and opportunities in Lever @@ -6375,16 +6411,19 @@ integrations: version: 1.0.0 get-stages: description: | - Action to get lists all pipeline stages in your Lever account. + Action to get lists all pipeline stages. Note that this does + not paginate the response so it is possible that not all stages + are returned. output: GetStages endpoint: method: GET - path: /stages - group: Opportunities + path: /stages/limited + group: Stages + version: 1.0.0 users: description: > Lists all the users in your Lever account. Only active users are - included by default.. + included by default. output: SuccessResponse endpoint: method: GET @@ -6392,29 +6431,33 @@ integrations: group: Users get-postings: description: | - Get all Posts for your account in Lever + Get all posts for your account. Note that this does + not paginate the response so it is possible that not all postings + are returned. output: SuccessResponse endpoint: method: GET - path: /posts/single + path: /posts/limited group: Posts + version: 1.0.0 get-archive-reasons: description: | - Get all Posts for your account in Lever + Get all archived reasons output: SuccessResponse endpoint: method: GET path: /archived/reasons - group: Opportunities - posting: + group: Archived + get-posting: description: | - Get single posts for your account in Lever + Get single post for your account in Lever output: SuccessResponse input: SinglePost endpoint: method: GET - path: /posts + path: /posts/single group: Posts + version: 1.0.0 update-opportunity-links: description: | Update the links in an opportunity @@ -6422,8 +6465,9 @@ integrations: input: UpdateLinks endpoint: method: POST - path: /links + path: /opportunities/links group: Opportunities + version: 1.0.0 update-opportunity-sources: description: | Update the sources in an opportunity @@ -6431,8 +6475,9 @@ integrations: input: UpdateSources endpoint: method: POST - path: /sources + path: /opportunities/sources group: Opportunities + version: 1.0.0 update-opportunity-stage: description: | Update the stage in an opportunity @@ -6440,8 +6485,9 @@ integrations: input: UpdateOpportunityStage endpoint: method: POST - path: /stages + path: /opportunities/stages group: Opportunities + version: 1.0.0 update-opportunity-tags: description: | Update the tags in an opportunity @@ -6449,8 +6495,9 @@ integrations: input: UpdateTags endpoint: method: POST - path: /tags + path: /opportunities/tags group: Opportunities + version: 1.0.0 update-opportunity-archived: description: | Update the tags in an opportunity @@ -6458,8 +6505,9 @@ integrations: input: ArchiveOpportunity endpoint: method: PUT - path: /archived + path: /opportunities/archived group: Opportunities + version: 1.0.0 apply-posting: description: > Submit an application on behalf of a candidate. This endpoint can only @@ -6500,54 +6548,63 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/opportunities-applications + path: /applications + group: Applications scopes: - applications:read:admin + version: 1.0.0 opportunities-feedbacks: runs: every 6 hours description: > - Fetches a list of all feedback forms for a candidate for a specific - Opportunity in lever + Fetches a list of all feedback forms for a candidate for every single + opportunity output: LeverOpportunityFeedback sync_type: full endpoint: method: GET - path: /lever/opportunities-feedbacks + path: /opportunities/feedback + group: Opportunities scopes: - feedback:read:admin opportunities-interviews: runs: every 6 hours description: | - Fetches a list of all interviewers for a specific Opportunity in lever + Fetches a list of all interviews for every single opportunity output: LeverOpportunityInterview sync_type: full endpoint: method: GET - path: /lever/opportunities-interviews + path: /opportunities/interviewers + group: Opportunities scopes: - interviews:read:admin + version: 1.0.0 opportunities-notes: runs: every 6 hours description: | - Fetches a list of all notes for a specific candidate in lever + Fetches a list of all notes for every single opportunity output: LeverOpportunityNote sync_type: full endpoint: method: GET - path: /lever/opportunities-notes + path: /opportunities-notes + group: Notes scopes: - notes:read:admin + version: 1.0.0 opportunities-offers: runs: every 6 hours description: | - Fetches a list of all offers for a specific candidate in lever. + Fetches a list of all offers for every single opportunity output: LeverOpportunityOffer sync_type: full endpoint: method: GET - path: /lever/opportunities-offers + path: /opportunities/offers + group: Offers scopes: - offers:write:admin + version: 1.0.0 postings: runs: every 6 hours description: | @@ -6556,10 +6613,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings + path: /postings + group: Postings scopes: - postings:read:admin - postings-apply: + version: 1.0.0 + postings-questions: runs: every 6 hours description: > Fetches a list of all questions included in a posting’s application @@ -6568,10 +6627,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings-apply + path: /postings/questions + group: Postings scopes: - postings:read:admin - stage: + version: 1.0.0 + stages: runs: every 6 hours description: | Fetches a list of all pipeline stages in Lever @@ -6579,9 +6640,11 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/stage + path: /stages + group: Stages scopes: - stages:read:admin + version: 1.0.0 models: LeverOpportunity: id: string @@ -6965,9 +7028,11 @@ integrations: input: LeverCreateNoteInput endpoint: method: POST - path: /lever/create-note + path: /notes + group: Notes scopes: - notes:write:admin + version: 1.0.0 create-opportunity: description: | Action to create candidates and opportunities in Lever @@ -6982,16 +7047,19 @@ integrations: version: 1.0.0 get-stages: description: | - Action to get lists all pipeline stages in your Lever account. + Action to get lists all pipeline stages. Note that this does + not paginate the response so it is possible that not all stages + are returned. output: GetStages endpoint: method: GET - path: /stages - group: Opportunities + path: /stages/limited + group: Stages + version: 1.0.0 users: description: > Lists all the users in your Lever account. Only active users are - included by default.. + included by default. output: SuccessResponse endpoint: method: GET @@ -6999,29 +7067,33 @@ integrations: group: Users get-postings: description: | - Get all Posts for your account in Lever + Get all posts for your account. Note that this does + not paginate the response so it is possible that not all postings + are returned. output: SuccessResponse endpoint: method: GET - path: /posts/single + path: /posts/limited group: Posts + version: 1.0.0 get-archive-reasons: description: | - Get all Posts for your account in Lever + Get all archived reasons output: SuccessResponse endpoint: method: GET path: /archived/reasons - group: Opportunities - posting: + group: Archived + get-posting: description: | - Get single posts for your account in Lever + Get single post for your account in Lever output: SuccessResponse input: SinglePost endpoint: method: GET - path: /posts + path: /posts/single group: Posts + version: 1.0.0 update-opportunity-links: description: | Update the links in an opportunity @@ -7029,8 +7101,9 @@ integrations: input: UpdateLinks endpoint: method: POST - path: /links + path: /opportunities/links group: Opportunities + version: 1.0.0 update-opportunity-sources: description: | Update the sources in an opportunity @@ -7038,8 +7111,9 @@ integrations: input: UpdateSources endpoint: method: POST - path: /sources + path: /opportunities/sources group: Opportunities + version: 1.0.0 update-opportunity-stage: description: | Update the stage in an opportunity @@ -7047,8 +7121,9 @@ integrations: input: UpdateOpportunityStage endpoint: method: POST - path: /stages + path: /opportunities/stages group: Opportunities + version: 1.0.0 update-opportunity-tags: description: | Update the tags in an opportunity @@ -7056,8 +7131,9 @@ integrations: input: UpdateTags endpoint: method: POST - path: /tags + path: /opportunities/tags group: Opportunities + version: 1.0.0 update-opportunity-archived: description: | Update the tags in an opportunity @@ -7065,8 +7141,9 @@ integrations: input: ArchiveOpportunity endpoint: method: PUT - path: /archived + path: /opportunities/archived group: Opportunities + version: 1.0.0 apply-posting: description: > Submit an application on behalf of a candidate. This endpoint can only @@ -7107,54 +7184,63 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/opportunities-applications + path: /applications + group: Applications scopes: - applications:read:admin + version: 1.0.0 opportunities-feedbacks: runs: every 6 hours description: > - Fetches a list of all feedback forms for a candidate for a specific - Opportunity in lever + Fetches a list of all feedback forms for a candidate for every single + opportunity output: LeverOpportunityFeedback sync_type: full endpoint: method: GET - path: /lever/opportunities-feedbacks + path: /opportunities/feedback + group: Opportunities scopes: - feedback:read:admin opportunities-interviews: runs: every 6 hours description: | - Fetches a list of all interviewers for a specific Opportunity in lever + Fetches a list of all interviews for every single opportunity output: LeverOpportunityInterview sync_type: full endpoint: method: GET - path: /lever/opportunities-interviews + path: /opportunities/interviewers + group: Opportunities scopes: - interviews:read:admin + version: 1.0.0 opportunities-notes: runs: every 6 hours description: | - Fetches a list of all notes for a specific candidate in lever + Fetches a list of all notes for every single opportunity output: LeverOpportunityNote sync_type: full endpoint: method: GET - path: /lever/opportunities-notes + path: /opportunities-notes + group: Notes scopes: - notes:read:admin + version: 1.0.0 opportunities-offers: runs: every 6 hours description: | - Fetches a list of all offers for a specific candidate in lever. + Fetches a list of all offers for every single opportunity output: LeverOpportunityOffer sync_type: full endpoint: method: GET - path: /lever/opportunities-offers + path: /opportunities/offers + group: Offers scopes: - offers:write:admin + version: 1.0.0 postings: runs: every 6 hours description: | @@ -7163,10 +7249,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings + path: /postings + group: Postings scopes: - postings:read:admin - postings-apply: + version: 1.0.0 + postings-questions: runs: every 6 hours description: > Fetches a list of all questions included in a posting’s application @@ -7175,10 +7263,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings-apply + path: /postings/questions + group: Postings scopes: - postings:read:admin - stage: + version: 1.0.0 + stages: runs: every 6 hours description: | Fetches a list of all pipeline stages in Lever @@ -7186,9 +7276,11 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/stage + path: /stages + group: Stages scopes: - stages:read:admin + version: 1.0.0 models: LeverOpportunity: id: string @@ -7572,9 +7664,11 @@ integrations: input: LeverCreateNoteInput endpoint: method: POST - path: /lever/create-note + path: /notes + group: Notes scopes: - notes:write:admin + version: 1.0.0 create-opportunity: description: | Action to create candidates and opportunities in Lever @@ -7589,16 +7683,19 @@ integrations: version: 1.0.0 get-stages: description: | - Action to get lists all pipeline stages in your Lever account. + Action to get lists all pipeline stages. Note that this does + not paginate the response so it is possible that not all stages + are returned. output: GetStages endpoint: method: GET - path: /stages - group: Opportunities + path: /stages/limited + group: Stages + version: 1.0.0 users: description: > Lists all the users in your Lever account. Only active users are - included by default.. + included by default. output: SuccessResponse endpoint: method: GET @@ -7606,29 +7703,33 @@ integrations: group: Users get-postings: description: | - Get all Posts for your account in Lever + Get all posts for your account. Note that this does + not paginate the response so it is possible that not all postings + are returned. output: SuccessResponse endpoint: method: GET - path: /posts/single + path: /posts/limited group: Posts + version: 1.0.0 get-archive-reasons: description: | - Get all Posts for your account in Lever + Get all archived reasons output: SuccessResponse endpoint: method: GET path: /archived/reasons - group: Opportunities - posting: + group: Archived + get-posting: description: | - Get single posts for your account in Lever + Get single post for your account in Lever output: SuccessResponse input: SinglePost endpoint: method: GET - path: /posts + path: /posts/single group: Posts + version: 1.0.0 update-opportunity-links: description: | Update the links in an opportunity @@ -7636,8 +7737,9 @@ integrations: input: UpdateLinks endpoint: method: POST - path: /links + path: /opportunities/links group: Opportunities + version: 1.0.0 update-opportunity-sources: description: | Update the sources in an opportunity @@ -7645,8 +7747,9 @@ integrations: input: UpdateSources endpoint: method: POST - path: /sources + path: /opportunities/sources group: Opportunities + version: 1.0.0 update-opportunity-stage: description: | Update the stage in an opportunity @@ -7654,8 +7757,9 @@ integrations: input: UpdateOpportunityStage endpoint: method: POST - path: /stages + path: /opportunities/stages group: Opportunities + version: 1.0.0 update-opportunity-tags: description: | Update the tags in an opportunity @@ -7663,8 +7767,9 @@ integrations: input: UpdateTags endpoint: method: POST - path: /tags + path: /opportunities/tags group: Opportunities + version: 1.0.0 update-opportunity-archived: description: | Update the tags in an opportunity @@ -7672,8 +7777,9 @@ integrations: input: ArchiveOpportunity endpoint: method: PUT - path: /archived + path: /opportunities/archived group: Opportunities + version: 1.0.0 apply-posting: description: > Submit an application on behalf of a candidate. This endpoint can only @@ -7714,54 +7820,63 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/opportunities-applications + path: /applications + group: Applications scopes: - applications:read:admin + version: 1.0.0 opportunities-feedbacks: runs: every 6 hours description: > - Fetches a list of all feedback forms for a candidate for a specific - Opportunity in lever + Fetches a list of all feedback forms for a candidate for every single + opportunity output: LeverOpportunityFeedback sync_type: full endpoint: method: GET - path: /lever/opportunities-feedbacks + path: /opportunities/feedback + group: Opportunities scopes: - feedback:read:admin opportunities-interviews: runs: every 6 hours description: | - Fetches a list of all interviewers for a specific Opportunity in lever + Fetches a list of all interviews for every single opportunity output: LeverOpportunityInterview sync_type: full endpoint: method: GET - path: /lever/opportunities-interviews + path: /opportunities/interviewers + group: Opportunities scopes: - interviews:read:admin + version: 1.0.0 opportunities-notes: runs: every 6 hours description: | - Fetches a list of all notes for a specific candidate in lever + Fetches a list of all notes for every single opportunity output: LeverOpportunityNote sync_type: full endpoint: method: GET - path: /lever/opportunities-notes + path: /opportunities-notes + group: Notes scopes: - notes:read:admin + version: 1.0.0 opportunities-offers: runs: every 6 hours description: | - Fetches a list of all offers for a specific candidate in lever. + Fetches a list of all offers for every single opportunity output: LeverOpportunityOffer sync_type: full endpoint: method: GET - path: /lever/opportunities-offers + path: /opportunities/offers + group: Offers scopes: - offers:write:admin + version: 1.0.0 postings: runs: every 6 hours description: | @@ -7770,10 +7885,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings + path: /postings + group: Postings scopes: - postings:read:admin - postings-apply: + version: 1.0.0 + postings-questions: runs: every 6 hours description: > Fetches a list of all questions included in a posting’s application @@ -7782,10 +7899,12 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/postings-apply + path: /postings/questions + group: Postings scopes: - postings:read:admin - stage: + version: 1.0.0 + stages: runs: every 6 hours description: | Fetches a list of all pipeline stages in Lever @@ -7793,9 +7912,11 @@ integrations: sync_type: full endpoint: method: GET - path: /lever/stage + path: /stages + group: Stages scopes: - stages:read:admin + version: 1.0.0 models: LeverOpportunity: id: string From cb0121850dacffc0826858dd0135f04d03989944 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Tue, 3 Dec 2024 12:22:03 +0000 Subject: [PATCH 02/45] chore(integration-templates): Automated commit updating flows.yaml based on changes in https://github.com/NangoHQ/integration-templates/commit/ef8abf3a9f6bd5c1e1afab8dc384036c66566713 by Daniel Roy Lwetabe. Commit message: feat(pennylane): Make penny lane syncs and actions into public templates (#127) --- packages/shared/flows.yaml | 577 +++++++++++++++++++++++++++++++++++++ 1 file changed, 577 insertions(+) diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index c7248cf81e..7b50e46fb5 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -9264,6 +9264,583 @@ integrations: DocumentInput: threadId: string attachmentId: string + pennylane: + actions: + create-customer: + description: | + Action to create a customer in pennylane + input: PennylaneIndividualCustomer + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: POST + path: /customers + group: Customers + create-invoice: + description: | + Action to create an invoice in pennylane + input: CreateInvoice + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: POST + path: /invoices + group: Invoices + create-supplier: + description: | + Action to create a supplier in pennylane + input: CreateSupplier + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: POST + path: /suppliers + group: Suppliers + update-customer: + description: | + Action to update a supplier in pennylane + input: UpdatePennylaneCustomer + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: PATCH + path: /customers + group: Customers + update-invoice: + description: | + Action to update an invoice in pennylane + input: UpdateInvoice + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: PATCH + path: /invoices + group: Invoices + update-supplier: + description: | + Action to update a supplier in pennylane + input: UpdateSupplier + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: PATCH + path: /suppliers + group: Suppliers + create-product: + description: | + Action to create a product in pennylane + input: CreateProduct + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: POST + path: /products + group: Products + update-product: + description: | + Action to update a product in pennylane + input: UpdateProduct + output: PennylaneSuccessResponse + version: 1.0.0 + endpoint: + method: PATCH + path: /products + group: Products + syncs: + customers: + runs: every 6 hours + description: | + Fetches a list of customers from pennylane + output: PennylaneCustomer + sync_type: incremental + endpoint: + method: GET + path: /customers + group: Customers + scopes: + - accounting + version: 1.0.0 + suppliers: + runs: every 6 hours + description: | + Fetches a list of suppliers from pennylane + output: PennylaneSupplier + sync_type: incremental + endpoint: + method: GET + path: /suppliers + group: Suppliers + scopes: + - supplier_invoices + version: 1.0.0 + invoices: + runs: every 6 hours + description: | + Fetches a list of customer invoices from pennylane + output: PennylaneInvoice + sync_type: incremental + endpoint: + method: GET + path: /invoices + group: Invoices + scopes: + - customer_invoices + version: 1.0.0 + products: + runs: every 6 hours + description: | + Fetches a list products from pennylane + output: PennylaneProduct + sync_type: incremental + endpoint: + method: GET + path: /products + group: Products + scopes: + - accounting + version: 1.0.0 + models: + CreateInvoice: + create_customer?: boolean + create_products?: boolean + update_customer?: boolean + date: string + deadline: string + draft?: boolean + customer_source_id: string + external_id?: string | null + pdf_invoice_free_text?: string | null + pdf_invoice_subject?: string | null + currency?: string + special_mention?: string | null + discount?: number + language?: string + transactions_reference?: TransactionReferenceObject + line_items: >- + LineItemWithTax[] | LineItemWithoutTax[] | + LineItemWithExistingProduct[] + categories?: CategoryObject[] + line_items_sections_attributes?: LineItemsSectionsAttributesObject[] + imputation_dates?: + start_date: string + end_date: string + UpdateInvoice: + id: string + label?: string | null + invoice_number?: string | null + quote_group_uuid?: string + is_draft?: boolean + is_estimate?: boolean + currency?: string + amount?: string + currency_amount?: string + currency_amount_before_tax?: string + exchange_rate?: number + date?: string | null + deadline?: string | null + currency_tax?: string + language?: string + paid?: boolean + fully_paid_at?: string | null + status?: string | null + discount?: string + discount_type?: string + public_url?: string + file_url?: string | null + filename?: string + remaining_amount?: string + source?: InvoiceSource + special_mention?: string | null + updated_at?: string + imputation_dates?: ImputationDateObject | null + customer?: PennylaneIndividualCustomer + line_items_sections_attributes?: LineItemsSectionsAttributesObject[] + line_items?: InvoiceLineItem[] + categories?: InvoiceCategory[] + transactions_reference?: TransactionReferenceObject + payments?: PaymentsObject[] + matched_transactions?: MatchedTransactionsObject[] + pdf_invoice_free_text?: string + pdf_invoice_subject?: string + billing_subscription?: BillingSubscriptionObject | null + UpdateInvoiceResponse: + invoice: UpdateInvoice + CreateSupplier: + name: string + reg_no?: string + address: string + postal_code: string + city: string + country_alpha2: string + recipient?: string + vat_number?: string + source_id?: string + emails: string[] + iban?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + UpdateSupplier: + source_id: string + name?: string + reg_no?: string + address?: string + postal_code?: string + city?: string + country_alpha2?: string + recipient?: string + vat_number?: string + emails?: string[] + iban?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + UpdateSupplierResponse: + supplier: + source_id: string + name?: string + reg_no?: string + address?: string + postal_code?: string + city?: string + country_alpha2?: string + recipient?: string + vat_number?: string + emails?: string[] + iban?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + InvoiceResponse: + invoice: PennylaneInvoice + PennylaneInvoice: + id: string + amount: string | null + billing_subscription?: BillingSubscriptionObject | null + categories?: InvoiceCategory[] | null + currency: string | null + currency_amount: string | null + currency_amount_before_tax?: string | null + currency_tax: string | null + customer?: PennylaneIndividualCustomer + customer_name: string + customer_validation_needed: boolean | null + date?: date | string + deadline: string | null + discount: string | null + discount_type?: string | null + exchange_rate: number | null + file_url: string | null + filename: string | null + fully_paid_at?: date | null + imputation_dates: ImputationDateObject | null + invoice_number?: string | null + is_draft: boolean + is_estimate?: boolean + label?: string | null + language?: string | null + line_items?: InvoiceLineItem[] + line_items_sections_attributes?: LineItemsSectionsAttributesObject[] + matched_transactions?: MatchedTransactionsObject[] + paid: boolean + payments: object[] + pdf_invoice_free_text: string + pdf_invoice_subject: string + public_url: string | null + quote_group_uuid?: string | null + remaining_amount: string | null + source: string | null + special_mention: string | null + status: string | null + transactions_reference?: TransactionReferenceObject | null + updated_at: date | string + LineItemWithTax: + label: string + quantity: number + section_rank?: number + currency_amount: number + plan_item_number?: string + unit: string + vat_rate: string + description?: string + discount?: number + LineItemWithoutTax: + label: string + quantity: number + section_rank?: number + currency_amount_before_tax: number + plan_item_number?: string + unit: string + vat_rate: string + description?: string + discount?: number + LineItemWithExistingProduct: + label?: string + quantity: number + discount?: number + section_rank?: number + plan_item_number?: string + product: + source_id: string + price?: number + vat_rate?: string + unit?: string + ImputationDateObject: + start_date: string + end_date: string + CategoryObject: + source_id: string + weight: number | null + amount: number | null + LineItemsSectionsAttributesObject: + title?: string | null + description?: string | null + rank: number + InvoiceLineItem: + id?: number + label?: string + unit?: string | null + quantity?: string + amount?: string + currency_amount?: string + description?: string + product_id?: string | null + vat_rate?: string + currency_price_before_tax?: string + currency_tax?: string + raw_currency_unit_price?: string + discount?: string + discount_type?: string + section_rank?: number | null + v2_id?: number | null + product_v2_id?: number | null + InvoiceCategory: + source_id: string + weight: string + label: string + direction: string | null + created_at: date | string + updated_at: date | string + TransactionReferenceObject: + banking_provider: string | null + provider_field_name: string | null + provider_field_value: string | null + PaymentsObject: + label: string + created_at: date | string + currency_amount: string + MatchedTransactionsObject: + label: string | null + amount: string | null + group_uuid: string | null + date: date | null + fee: string | null + currency: string + BillingSubscriptionObject: + id: string | null + IndividualCustomer: PennylaneIndividualCustomer + UpdateIndividualCustomer: + id: string + customer: PennylaneIndividualCustomer + IndividualCustomerResponse: + customer: + first_name?: string + last_name?: string + gender?: string | null + name?: string + updated_at?: string + source_id: string + emails?: string[] + billing_iban?: string | null + customer_type?: string + recipient?: string + billing_address?: + address?: string + postal_code?: string + city?: string + country_alpha2?: string + delivery_address?: + address?: string + postal_code?: string + city?: string + country_alpha2?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + plan_item?: + number: string + label: string + enabled: boolean + vat_rate: string + country_alpha2: string + description: string + mandates?: MandateObject[] + MandateObject: + provider: string + source_id: string + PennylaneIndividualCustomer: + customer_type?: string + first_name: string + last_name: string + country_alpha2: string + gender?: string | null + address?: string + postal_code?: string + city?: string + source_id?: string + emails?: string[] + billing_iban?: string + delivery_address?: string | DeliveryAddressObject + vat_number?: string | null + delivery_postal_code?: string + delivery_city?: string + delivery_country_alpha2?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + mandate?: + provider?: string + source_id: string + plan_item?: + number?: string + label?: string + enabled?: boolean + vat_rate?: string + country_alpha2?: string + description?: string + PennylaneSuccessResponse: + success: boolean + source_id: string + UpdatePennylaneCustomer: + id: string + first_name?: string + last_name?: string + gender?: string + address?: string + vat_number?: string | null + postal_code?: string | null + city?: string | null + country_alpha2?: string | null + recipient?: string | null + source_id?: string | null + emails?: string[] | null + billing_iban?: string | null + delivery_address?: DeliveryAddressObject | null + delivery_postal_code?: string | null + delivery_country?: string | null + delivery_country_alpha2?: string | null + payment_conditions?: string | null + phone?: string | null + reference?: string | null + notes?: string | null + DeliveryAddressObject: + address?: string + postal_code?: string | null + city?: string | null + country_alpha2?: string | null + PennylaneCustomer: + id: string + first_name?: string + last_name?: string + gender?: string + address?: string + vat_number?: string | null + postal_code?: string | null + city?: string | null + country_alpha2?: string | null + recipient?: string | null + source_id?: string | null + emails?: string[] | null + billing_iban?: string | null + delivery_address?: DeliveryAddressObject | null + delivery_postal_code?: string | null + delivery_country_alpha2?: string | null + payment_conditions?: string | null + phone?: string | null + reference?: string | null + notes?: string | null + PennylaneSupplier: + name: string + id?: string + reg_no?: string + address: string + postal_code: string + city: string + country_alpha2: string + recipient?: string + vat_number?: string + source_id?: string + emails: string[] + iban?: string + payment_conditions?: string + phone?: string + reference?: string + notes?: string + CreateProduct: + source_id: string + label: string + description?: string + unit: string + price_before_tax?: number + price: number + vat_rate: string + currency: string + reference?: string | null + substance?: string | null + UpdateProduct: + source_id: string + label?: string + description?: string + unit?: string + price_before_tax?: number + price?: number + vat_rate?: string + currency?: string + reference?: string | null + substance?: string | null + PennylaneProduct: + id: string + source_id: string + label: string + description?: string + unit: string + price_before_tax?: number + price: number + vat_rate: string + currency: string + reference?: string | null + substance?: string | null + InvoiceMapper: + create_customer: boolean + create_products: boolean + update_customer: boolean + invoice: + date: string + deadline: string + draft: boolean + customer: + source_id: string + currency: string + line_items: >- + LineItemWithTax[] | LineItemWithoutTax[] | + LineItemWithExistingProduct[] + pdf_invoice_free_text: string + pdf_invoice_subject: string + special_mention: string | null + discount: number + categories: CategoryObject[] | null + transactions_reference?: + banking_provider: string + provider_field_name: string + provider_field_value: string + imputation_dates?: + start_date: string + end_date: string perimeter81: actions: create-user: From b97bcfd0b0732b1b3a67ef3bbbdcc86addbac307 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Tue, 3 Dec 2024 14:35:53 +0200 Subject: [PATCH 03/45] fix(providers-yaml): add automated for additional salesforce (#3102) Update `salesforce-experience-cloud` to also have an `automated: true` property --- packages/shared/providers.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/providers.yaml b/packages/shared/providers.yaml index 4f02adf05e..0f686cddce 100644 --- a/packages/shared/providers.yaml +++ b/packages/shared/providers.yaml @@ -5236,6 +5236,7 @@ salesforce-experience-cloud: description: The instance URL of your Salesforce Experience Cloud account format: uri pattern: '^https?://.*$' + automated: true salesloft: display_name: Salesloft From 5608eaa0a23772aa1ebc9c3d047356cc8c155232 Mon Sep 17 00:00:00 2001 From: nalanj <5594+nalanj@users.noreply.github.com> Date: Tue, 3 Dec 2024 09:11:08 -0500 Subject: [PATCH 04/45] fix(server): Load errorLog regardless of credential refresh result (#3103) When viewing a connection we had a strange state that was possible where there was an auth error on a connection but because it was not a connection that could be refreshed it wasn't showing the error in the UI once viewing the settings. See https://linear.app/nango/issue/NAN-2255/authorization-tab-for-connections-sometimes-doesnt-show-auth-errors ## How I tested it I set up an auth error row in my test db on a connection that didn't refresh (I used Unauthenticated) and confirmed that it displayed correctly. Here's a sample insert (you'll need to tweak it to get it to work with a connection you have set up): ``` INSERT INTO "nango"."_nango_active_logs"("id","type","action","connection_id","log_id","active","sync_id","created_at","updated_at") VALUES (391,E'auth',E'token_refresh',73,E'begpVdnwQLenQDHNVicf',TRUE,NULL,E'2024-11-26 16:53:33.628375+00',E'2024-11-26 16:53:33.628375+00'); ``` Before this change you would see the auth error in the connections list, but visiting the connection would not show a dot on the auth tab or show an error on that page. After this change you'll see the dot and the error. --- .../connections/connectionId/getConnection.ts | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts b/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts index 3a0ce9df7f..4e7bb7faec 100644 --- a/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts +++ b/packages/server/lib/controllers/v1/connections/connectionId/getConnection.ts @@ -83,31 +83,18 @@ export const getConnection = asyncWrapper(async (req, res) => { onRefreshSuccess: connectionRefreshSuccessHook, onRefreshFailed: connectionRefreshFailedHook }); - if (credentialResponse.isErr()) { - const errorLog = await errorNotificationService.auth.get(connection.id!); - // When we failed to refresh we still return a 200 because the connection is used in the UI - // Ultimately this could be a second endpoint so the UI displays faster and no confusion between error code - res.status(200).send({ - data: { - errorLog, - provider: integration.provider, - connection: connectionFullToApi(connection as DBConnection), - endUser: endUserToApi(endUser) - } - }); - - return; + if (credentialResponse.isOk()) { + connection = credentialResponse.value; } - - connection = credentialResponse.value; + const errorLog = await errorNotificationService.auth.get(connection.id!); res.status(200).send({ data: { provider: integration.provider, connection: connectionFullToApi(connection as DBConnection), endUser: endUserToApi(endUser), - errorLog: null + errorLog } }); }); From 564a7031f14377cf86bc16b3545aedda1213555e Mon Sep 17 00:00:00 2001 From: Hassan_Wari <85742599+hassan254-prog@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:29:35 +0300 Subject: [PATCH 05/45] fix(proxy): handle more content-disposition edge cases (#3100) Refactored the response handling to cover more edge cases. Instead of checking if the `content-disposition` header is exactly equal to `attachment`, we now check if it includes `attachment` or `inline`, allowing for more flexibility. This change improves the handling of different content types by considering both attachment and inline dispositions. Initially this was producing a malformed response with the dropbox and front providers, but after the fix, it works as expected. --------- Co-authored-by: Hassan Wari Co-authored-by: Khaliq --- .../server/lib/controllers/proxy.controller.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/server/lib/controllers/proxy.controller.ts b/packages/server/lib/controllers/proxy.controller.ts index b7e58660ac..4d56c6041a 100644 --- a/packages/server/lib/controllers/proxy.controller.ts +++ b/packages/server/lib/controllers/proxy.controller.ts @@ -265,13 +265,17 @@ class ProxyController { } }); - const contentType = responseStream.headers['content-type']; - const isJsonResponse = contentType && contentType.includes('application/json'); - const isChunked = responseStream.headers['transfer-encoding'] === 'chunked'; - const isEncoded = Boolean(responseStream.headers['content-encoding']); - const isAttachment = responseStream.headers['content-disposition'] === 'attachment'; + const contentType = responseStream.headers['content-type'] || ''; + const contentDisposition = responseStream.headers['content-disposition'] || ''; + const transferEncoding = responseStream.headers['transfer-encoding'] || ''; + const contentEncoding = responseStream.headers['content-encoding'] || ''; - if (isChunked || isEncoded || isAttachment) { + const isJsonResponse = contentType.includes('application/json'); + const isChunked = transferEncoding === 'chunked'; + const isEncoded = Boolean(contentEncoding); + const isAttachmentOrInline = /^(attachment|inline)(;|\s|$)/i.test(contentDisposition); + + if (isChunked || isEncoded || isAttachmentOrInline) { const passThroughStream = new PassThrough(); responseStream.data.pipe(passThroughStream); passThroughStream.pipe(res); From 96d0ff5790cc0bc9b8b9aa3da5fb3f3a6fb985fc Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Tue, 3 Dec 2024 18:38:30 +0000 Subject: [PATCH 06/45] chore(integration-templates): Automated commit updating flows.yaml based on changes in https://github.com/NangoHQ/integration-templates/commit/fe1184f19c3f580a309f7135699adf788e76695a by Khaliq. Commit message: fix(lever): more cleanup of endpoints (#134) --- packages/shared/flows.yaml | 84 ++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index 7b50e46fb5..ea52fd3d4e 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -5751,7 +5751,7 @@ integrations: actions: create-note: description: | - Action to create a note and add it to a candidate profile in Lever + Action to create a note and add it to an opportunity. output: LeverOpportunityNote input: LeverCreateNoteInput endpoint: @@ -5762,8 +5762,9 @@ integrations: - notes:write:admin version: 1.0.0 create-opportunity: - description: | - Action to create candidates and opportunities in Lever + description: > + Create an opportunity and optionally candidates associated with the + opportunity output: LeverOpportunity input: LeverCreateOpportunityInput endpoint: @@ -5864,7 +5865,7 @@ integrations: version: 1.0.0 update-opportunity-archived: description: | - Update the tags in an opportunity + Update the archived state of an opportunity output: SuccessResponse input: ArchiveOpportunity endpoint: @@ -5888,14 +5889,14 @@ integrations: output: ReturnObjUpdateOpportunity input: UpdateOpportunity endpoint: - method: POST - path: /opportunity/update + method: PATCH + path: /opportunities group: Opportunities syncs: opportunities: runs: every 6 hours description: | - Fetches a list of all pipeline opportunities for contacts in Lever + Fetches al opportunities output: LeverOpportunity sync_type: incremental endpoint: @@ -5938,7 +5939,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/interviewers + path: /opportunities/interviews group: Opportunities scopes: - interviews:read:admin @@ -5951,7 +5952,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities-notes + path: /notes group: Notes scopes: - notes:read:admin @@ -5964,7 +5965,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/offers + path: /offers group: Offers scopes: - offers:write:admin @@ -6387,7 +6388,7 @@ integrations: actions: create-note: description: | - Action to create a note and add it to a candidate profile in Lever + Action to create a note and add it to an opportunity. output: LeverOpportunityNote input: LeverCreateNoteInput endpoint: @@ -6398,8 +6399,9 @@ integrations: - notes:write:admin version: 1.0.0 create-opportunity: - description: | - Action to create candidates and opportunities in Lever + description: > + Create an opportunity and optionally candidates associated with the + opportunity output: LeverOpportunity input: LeverCreateOpportunityInput endpoint: @@ -6500,7 +6502,7 @@ integrations: version: 1.0.0 update-opportunity-archived: description: | - Update the tags in an opportunity + Update the archived state of an opportunity output: SuccessResponse input: ArchiveOpportunity endpoint: @@ -6524,14 +6526,14 @@ integrations: output: ReturnObjUpdateOpportunity input: UpdateOpportunity endpoint: - method: POST - path: /opportunity/update + method: PATCH + path: /opportunities group: Opportunities syncs: opportunities: runs: every 6 hours description: | - Fetches a list of all pipeline opportunities for contacts in Lever + Fetches al opportunities output: LeverOpportunity sync_type: incremental endpoint: @@ -6574,7 +6576,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/interviewers + path: /opportunities/interviews group: Opportunities scopes: - interviews:read:admin @@ -6587,7 +6589,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities-notes + path: /notes group: Notes scopes: - notes:read:admin @@ -6600,7 +6602,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/offers + path: /offers group: Offers scopes: - offers:write:admin @@ -7023,7 +7025,7 @@ integrations: actions: create-note: description: | - Action to create a note and add it to a candidate profile in Lever + Action to create a note and add it to an opportunity. output: LeverOpportunityNote input: LeverCreateNoteInput endpoint: @@ -7034,8 +7036,9 @@ integrations: - notes:write:admin version: 1.0.0 create-opportunity: - description: | - Action to create candidates and opportunities in Lever + description: > + Create an opportunity and optionally candidates associated with the + opportunity output: LeverOpportunity input: LeverCreateOpportunityInput endpoint: @@ -7136,7 +7139,7 @@ integrations: version: 1.0.0 update-opportunity-archived: description: | - Update the tags in an opportunity + Update the archived state of an opportunity output: SuccessResponse input: ArchiveOpportunity endpoint: @@ -7160,14 +7163,14 @@ integrations: output: ReturnObjUpdateOpportunity input: UpdateOpportunity endpoint: - method: POST - path: /opportunity/update + method: PATCH + path: /opportunities group: Opportunities syncs: opportunities: runs: every 6 hours description: | - Fetches a list of all pipeline opportunities for contacts in Lever + Fetches al opportunities output: LeverOpportunity sync_type: incremental endpoint: @@ -7210,7 +7213,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/interviewers + path: /opportunities/interviews group: Opportunities scopes: - interviews:read:admin @@ -7223,7 +7226,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities-notes + path: /notes group: Notes scopes: - notes:read:admin @@ -7236,7 +7239,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/offers + path: /offers group: Offers scopes: - offers:write:admin @@ -7659,7 +7662,7 @@ integrations: actions: create-note: description: | - Action to create a note and add it to a candidate profile in Lever + Action to create a note and add it to an opportunity. output: LeverOpportunityNote input: LeverCreateNoteInput endpoint: @@ -7670,8 +7673,9 @@ integrations: - notes:write:admin version: 1.0.0 create-opportunity: - description: | - Action to create candidates and opportunities in Lever + description: > + Create an opportunity and optionally candidates associated with the + opportunity output: LeverOpportunity input: LeverCreateOpportunityInput endpoint: @@ -7772,7 +7776,7 @@ integrations: version: 1.0.0 update-opportunity-archived: description: | - Update the tags in an opportunity + Update the archived state of an opportunity output: SuccessResponse input: ArchiveOpportunity endpoint: @@ -7796,14 +7800,14 @@ integrations: output: ReturnObjUpdateOpportunity input: UpdateOpportunity endpoint: - method: POST - path: /opportunity/update + method: PATCH + path: /opportunities group: Opportunities syncs: opportunities: runs: every 6 hours description: | - Fetches a list of all pipeline opportunities for contacts in Lever + Fetches al opportunities output: LeverOpportunity sync_type: incremental endpoint: @@ -7846,7 +7850,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/interviewers + path: /opportunities/interviews group: Opportunities scopes: - interviews:read:admin @@ -7859,7 +7863,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities-notes + path: /notes group: Notes scopes: - notes:read:admin @@ -7872,7 +7876,7 @@ integrations: sync_type: full endpoint: method: GET - path: /opportunities/offers + path: /offers group: Offers scopes: - offers:write:admin From 3cfdc91aec3bd0908ff8c536e99bbd24f517268a Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Tue, 3 Dec 2024 19:12:13 +0000 Subject: [PATCH 07/45] chore(integration-templates): Automated commit updating flows.yaml based on changes in https://github.com/NangoHQ/integration-templates/commit/06bf1271d4b2d7d4226f69a78d0883c51ae7f6f0 by Khaliq. Commit message: feat(avalara): Add avalara syncs and actions (#133) --- packages/shared/flows.yaml | 536 +++++++++++++++++++++++++++++++++++++ 1 file changed, 536 insertions(+) diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index ea52fd3d4e..3b5a486d6a 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -956,6 +956,542 @@ integrations: updatedAt: date hiringTeam: HiringTeamObject[] appliedViaJobPostingId?: string + avalara: + actions: + create-transaction: + description: | + Creates a new transaction + input: CreateTransaction + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, AccountUser, BatchServiceAdmin, + CompanyAdmin, CompanyUser, CSPTester, SSTAdmin, TechnicalSupportAdmin, + TechnicalSupportUser + endpoint: + method: POST + path: /transactions + group: Transactions + commit-transaction: + description: | + Marks a transaction by changing its status to Committed + input: TransactionCode + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, AccountUser, BatchServiceAdmin, + CompanyAdmin, CompanyUser, CSPTester, ProStoresOperator, SSTAdmin, + TechnicalSupportAdmin + endpoint: + method: PUT + path: /transactions + group: Transactions + void-transaction: + description: > + Voids the current transaction uniquely identified by the + transactionCode + input: TransactionCode + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, BatchServiceAdmin, CompanyAdmin, + CSPTester, ProStoresOperator, SSTAdmin, TechnicalSupportAdmin + endpoint: + method: DELETE + path: /transactions + group: Transactions + syncs: + transactions: + description: List all transactions with a default backfill date of one year. + input: ConnectionMetadata + output: Transaction + runs: every hour + endpoint: + method: GET + path: /transactions + group: Transactions + models: + IdEntity: + id: string + ConnectionMetadata: + company: string + backfillPeriodMs?: number + Transaction: + id: string + code: string + companyId: number + date: string + paymentDate: string + status: string + type: string + batchCode: string + currencyCode: string + exchangeRateCurrencyCode: string + customerUsageType: string + entityUseCode: string + customerVendorCode: string + customerCode: string + exemptNo: string + reconciled: boolean + locationCode: string + reportingLocationCode: string + purchaseOrderNo: string + referenceCode: string + salespersonCode: string + taxOverrideType: string + taxOverrideAmount: number + taxOverrideReason: string + totalAmount: number + totalExempt: number + totalDiscount: number + totalTax: number + totalTaxable: number + totalTaxCalculated: number + adjustmentReason: string + adjustmentDescription: string + locked: boolean + region: string + country: string + version: number + softwareVersion: string + originAddressId: number + destinationAddressId: number + exchangeRateEffectiveDate: string + exchangeRate: number + isSellerImporterOfRecord: boolean + description: string + email: string + businessIdentificationNo: string + modifiedDate: string + modifiedUserId: number + taxDate: string + lines: Line[] + locationTypes: any[] + messages: string[] + summary: string[] + addresses?: TransactionAddress[] + taxDetailsByTaxType?: TaxDetailsByTaxType[] + Line: + id: number + transactionId: number + lineNumber: string + boundaryOverrideId: number + entityUseCode: string + description: string + destinationAddressId: number + originAddressId: number + discountAmount: number + discountTypeId: number + exemptAmount: number + exemptCertId: number + exemptNo: string + isItemTaxable: boolean + isSSTP: boolean + itemCode: string + lineAmount: number + quantity: number + ref1: string + reportingDate: string + revAccount: string + sourcing: string + tax: number + taxableAmount: number + taxCalculated: number + taxCode: string + taxDate: string + taxEngine: string + taxOverrideType: string + taxOverrideAmount: number + taxOverrideReason: string + taxIncluded: boolean + details: Detail[] + vatNumberTypeId: number + recoverabilityPercentage: number + recoverableAmount: number + nonRecoverableAmount: number + Detail: + id: number + transactionLineId: number + transactionId: number + addressId: number + country: string + region: string + stateFIPS: string + exemptAmount: number + exemptReasonId: number + exemptRuleId: number + inState: boolean + jurisCode: string + jurisName: string + jurisdictionId: number + signatureCode: string + stateAssignedNo: string + jurisType: string + nonTaxableAmount: number + nonTaxableRuleId: number + nonTaxableType: string + rate: number + rateRuleId: number + rateSourceId: number + serCode: string + sourcing: string + tax: number + taxableAmount: number + taxType: string + taxName: string + taxAuthorityTypeId: number + taxRegionId: number + taxCalculated: number + taxOverride: number + rateType: string + taxableUnits: number + nonTaxableUnits: number + exemptUnits: number + reportingTaxableUnits: number + reportingNonTaxableUnits: number + reportingExemptUnits: number + reportingTax: number + reportingTaxCalculated: number + recoverabilityPercentage: number + recoverableAmount: number + nonRecoverableAmount: number + TransactionAddress: + id: number + transactionId: number + boundaryLevel: string + line1: string + city: string + region: string + postalCode: string + country: string + taxRegionId: number + TaxDetailsByTaxType: + taxType: string + totalTaxable: number + totalExempt: number + totalNonTaxable: number + totalTax: number + CreateTransaction: + invoice: Invoice + externalCustomerId: string + companyCode?: string + addresses: + singleLocation?: Address + shipFrom?: Address + shipTo?: Address + pointOfOrderOrigin?: Address + pointOfOrderAcceptance?: Address + goodsPlaceOrServiceRendered?: Address + import?: Address + billTo?: Address + TransactionCode: + transactionCode: string + Address: + line1?: string + city?: string + region?: string + country?: string + postalCode?: string + InvoiceCoupon: + name: string + discountAmount: number + InvoiceLineItemTier: + unitCount: string + unitAmount: string + totalAmount: number + InvoiceLineItem: + id?: string | undefined + billingItemId?: string | null | undefined + name: string + description: string | null + unitsCount: number + unitAmount: string + taxAmount: number + taxRate: string + amount?: number | undefined + amountExcludingTax: number + periodStart: string | null + periodEnd: string | null + invoiceLineItemTiers: InvoiceLineItemTier[] + Invoice: + id: string + invoiceNumber: string + emissionDate: string + dueDate: string + status: >- + to_pay | partially_paid | paid | late | grace_period | to_pay_batch | + voided + taxRate: string + currency: string + invoiceLineItems: InvoiceLineItem[] + coupons: InvoiceCoupon[] + type: invoice | refund + discountAmount: number + avalara-sandbox: + actions: + create-transaction: + description: | + Creates a new transaction + input: CreateTransaction + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, AccountUser, BatchServiceAdmin, + CompanyAdmin, CompanyUser, CSPTester, SSTAdmin, TechnicalSupportAdmin, + TechnicalSupportUser + endpoint: + method: POST + path: /transactions + group: Transactions + commit-transaction: + description: | + Marks a transaction by changing its status to Committed + input: TransactionCode + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, AccountUser, BatchServiceAdmin, + CompanyAdmin, CompanyUser, CSPTester, ProStoresOperator, SSTAdmin, + TechnicalSupportAdmin + endpoint: + method: PUT + path: /transactions + group: Transactions + void-transaction: + description: > + Voids the current transaction uniquely identified by the + transactionCode + input: TransactionCode + output: IdEntity + scopes: >- + AccountAdmin, AccountOperator, BatchServiceAdmin, CompanyAdmin, + CSPTester, ProStoresOperator, SSTAdmin, TechnicalSupportAdmin + endpoint: + method: DELETE + path: /transactions + group: Transactions + syncs: + transactions: + description: List all transactions with a default backfill date of one year. + input: ConnectionMetadata + output: Transaction + runs: every hour + endpoint: + method: GET + path: /transactions + group: Transactions + models: + IdEntity: + id: string + ConnectionMetadata: + company: string + backfillPeriodMs?: number + Transaction: + id: string + code: string + companyId: number + date: string + paymentDate: string + status: string + type: string + batchCode: string + currencyCode: string + exchangeRateCurrencyCode: string + customerUsageType: string + entityUseCode: string + customerVendorCode: string + customerCode: string + exemptNo: string + reconciled: boolean + locationCode: string + reportingLocationCode: string + purchaseOrderNo: string + referenceCode: string + salespersonCode: string + taxOverrideType: string + taxOverrideAmount: number + taxOverrideReason: string + totalAmount: number + totalExempt: number + totalDiscount: number + totalTax: number + totalTaxable: number + totalTaxCalculated: number + adjustmentReason: string + adjustmentDescription: string + locked: boolean + region: string + country: string + version: number + softwareVersion: string + originAddressId: number + destinationAddressId: number + exchangeRateEffectiveDate: string + exchangeRate: number + isSellerImporterOfRecord: boolean + description: string + email: string + businessIdentificationNo: string + modifiedDate: string + modifiedUserId: number + taxDate: string + lines: Line[] + locationTypes: any[] + messages: string[] + summary: string[] + addresses?: TransactionAddress[] + taxDetailsByTaxType?: TaxDetailsByTaxType[] + Line: + id: number + transactionId: number + lineNumber: string + boundaryOverrideId: number + entityUseCode: string + description: string + destinationAddressId: number + originAddressId: number + discountAmount: number + discountTypeId: number + exemptAmount: number + exemptCertId: number + exemptNo: string + isItemTaxable: boolean + isSSTP: boolean + itemCode: string + lineAmount: number + quantity: number + ref1: string + reportingDate: string + revAccount: string + sourcing: string + tax: number + taxableAmount: number + taxCalculated: number + taxCode: string + taxDate: string + taxEngine: string + taxOverrideType: string + taxOverrideAmount: number + taxOverrideReason: string + taxIncluded: boolean + details: Detail[] + vatNumberTypeId: number + recoverabilityPercentage: number + recoverableAmount: number + nonRecoverableAmount: number + Detail: + id: number + transactionLineId: number + transactionId: number + addressId: number + country: string + region: string + stateFIPS: string + exemptAmount: number + exemptReasonId: number + exemptRuleId: number + inState: boolean + jurisCode: string + jurisName: string + jurisdictionId: number + signatureCode: string + stateAssignedNo: string + jurisType: string + nonTaxableAmount: number + nonTaxableRuleId: number + nonTaxableType: string + rate: number + rateRuleId: number + rateSourceId: number + serCode: string + sourcing: string + tax: number + taxableAmount: number + taxType: string + taxName: string + taxAuthorityTypeId: number + taxRegionId: number + taxCalculated: number + taxOverride: number + rateType: string + taxableUnits: number + nonTaxableUnits: number + exemptUnits: number + reportingTaxableUnits: number + reportingNonTaxableUnits: number + reportingExemptUnits: number + reportingTax: number + reportingTaxCalculated: number + recoverabilityPercentage: number + recoverableAmount: number + nonRecoverableAmount: number + TransactionAddress: + id: number + transactionId: number + boundaryLevel: string + line1: string + city: string + region: string + postalCode: string + country: string + taxRegionId: number + TaxDetailsByTaxType: + taxType: string + totalTaxable: number + totalExempt: number + totalNonTaxable: number + totalTax: number + CreateTransaction: + invoice: Invoice + externalCustomerId: string + companyCode?: string + addresses: + singleLocation?: Address + shipFrom?: Address + shipTo?: Address + pointOfOrderOrigin?: Address + pointOfOrderAcceptance?: Address + goodsPlaceOrServiceRendered?: Address + import?: Address + billTo?: Address + TransactionCode: + transactionCode: string + Address: + line1?: string + city?: string + region?: string + country?: string + postalCode?: string + InvoiceCoupon: + name: string + discountAmount: number + InvoiceLineItemTier: + unitCount: string + unitAmount: string + totalAmount: number + InvoiceLineItem: + id?: string | undefined + billingItemId?: string | null | undefined + name: string + description: string | null + unitsCount: number + unitAmount: string + taxAmount: number + taxRate: string + amount?: number | undefined + amountExcludingTax: number + periodStart: string | null + periodEnd: string | null + invoiceLineItemTiers: InvoiceLineItemTier[] + Invoice: + id: string + invoiceNumber: string + emissionDate: string + dueDate: string + status: >- + to_pay | partially_paid | paid | late | grace_period | to_pay_batch | + voided + taxRate: string + currency: string + invoiceLineItems: InvoiceLineItem[] + coupons: InvoiceCoupon[] + type: invoice | refund + discountAmount: number aws-iam: actions: create-user: From 4054d735afa897583364fe85404a961878c95f83 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Wed, 4 Dec 2024 04:34:41 +0000 Subject: [PATCH 08/45] chore(integration-templates): Automated commit updating flows.yaml based on changes in https://github.com/NangoHQ/integration-templates/commit/5f28b046494e2ddaaa522bcb75bfaf78fe71db22 by Hassan_Wari. Commit message: fix(front): add cursor in request param for pagination (#135) --- packages/shared/flows.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/shared/flows.yaml b/packages/shared/flows.yaml index 3b5a486d6a..4348158e98 100644 --- a/packages/shared/flows.yaml +++ b/packages/shared/flows.yaml @@ -3456,6 +3456,7 @@ integrations: path: /conversations track_deletes: true sync_type: full + version: 1.0.1 actions: conversation: description: >- From e238a7b5ab0b72e6e096e503444cc23aad395f4d Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:26:46 +0100 Subject: [PATCH 09/45] feat(connect): allow passing oauth_scopes (#3104) ## Changes Fixes https://linear.app/nango/issue/NAN-2304/connect-support-oauth-scopes - Support passing `oauth_scopes_override` and `user_scopes` ## Tests I don't know, if you have an idea of an integration that supports this let me know :D --- docs-v2/spec.yaml | 9 +++++++++ .../lib/controllers/auth/postUnauthenticated.ts | 3 +-- .../lib/controllers/connect/postSessions.ts | 12 ++++++++++-- .../server/lib/controllers/oauth.controller.ts | 17 ++++++++++++++--- packages/types/lib/connect/api.ts | 13 ++++++++++++- packages/types/lib/connect/session.ts | 12 +++++++++++- 6 files changed, 57 insertions(+), 9 deletions(-) diff --git a/docs-v2/spec.yaml b/docs-v2/spec.yaml index 2e9ba6cc6f..18def98f19 100644 --- a/docs-v2/spec.yaml +++ b/docs-v2/spec.yaml @@ -2158,6 +2158,15 @@ components: additionalProperties: type: object description: the unique name of an integration + additionalProperties: false properties: + user_scopes: + type: string + description: User scopes (for Slack only) connection_config: type: object + additionalProperties: true + properties: + oauth_scopes_override: + type: string + description: Override oauth scopes diff --git a/packages/server/lib/controllers/auth/postUnauthenticated.ts b/packages/server/lib/controllers/auth/postUnauthenticated.ts index ad6fe9d6b4..fa1f6baacf 100644 --- a/packages/server/lib/controllers/auth/postUnauthenticated.ts +++ b/packages/server/lib/controllers/auth/postUnauthenticated.ts @@ -15,8 +15,7 @@ import { isIntegrationAllowed } from '../../utils/auth.js'; const queryStringValidation = z .object({ connection_id: connectionIdSchema.optional(), - params: z.record(z.any()).optional(), - user_scope: z.string().optional() + params: z.record(z.any()).optional() }) .and(connectionCredential); diff --git a/packages/server/lib/controllers/connect/postSessions.ts b/packages/server/lib/controllers/connect/postSessions.ts index f173b93f94..f83b3490fb 100644 --- a/packages/server/lib/controllers/connect/postSessions.ts +++ b/packages/server/lib/controllers/connect/postSessions.ts @@ -28,7 +28,12 @@ const bodySchema = z .record( z .object({ - connection_config: z.record(z.unknown()) + user_scopes: z.string().optional(), + connection_config: z + .object({ + oauth_scopes_override: z.string().optional() + }) + .passthrough() }) .strict() ) @@ -120,7 +125,10 @@ export const postConnectSessions = asyncWrapper(async (req, allowedIntegrations: req.body.allowed_integrations || null, integrationsConfigDefaults: req.body.integrations_config_defaults ? Object.fromEntries( - Object.entries(req.body.integrations_config_defaults).map(([key, value]) => [key, { connectionConfig: value.connection_config }]) + Object.entries(req.body.integrations_config_defaults).map(([key, value]) => [ + key, + { user_scopes: value.user_scopes, connectionConfig: value.connection_config } + ]) ) : null }); diff --git a/packages/server/lib/controllers/oauth.controller.ts b/packages/server/lib/controllers/oauth.controller.ts index eb4104d020..b61caf99d7 100644 --- a/packages/server/lib/controllers/oauth.controller.ts +++ b/packages/server/lib/controllers/oauth.controller.ts @@ -62,7 +62,7 @@ class OAuthController { const { providerConfigKey } = req.params; const receivedConnectionId = req.query['connection_id'] as string | undefined; const wsClientId = req.query['ws_client_id'] as string | undefined; - const userScope = req.query['user_scope'] as string | undefined; + let userScope = req.query['user_scope'] as string | undefined; const isConnectSession = res.locals['authType'] === 'connectSession'; let logCtx: LogContext | undefined; @@ -149,6 +149,12 @@ class OAuthController { return; } + if (isConnectSession) { + // Session token always win + const defaults = res.locals.connectSession.integrationsConfigDefaults?.[config.unique_key]; + userScope = defaults?.user_scopes || undefined; + } + const session: OAuthSession = { providerConfigKey: providerConfigKey, provider: config.provider, @@ -195,7 +201,12 @@ class OAuthController { }); } - if (connectionConfig['oauth_scopes_override']) { + if (isConnectSession) { + const defaults = res.locals.connectSession.integrationsConfigDefaults?.[config.unique_key]; + if (defaults?.connectionConfig.oauth_scopes_override) { + config.oauth_scopes = defaults?.connectionConfig.oauth_scopes_override; + } + } else if (connectionConfig['oauth_scopes_override']) { config.oauth_scopes = connectionConfig['oauth_scopes_override']; } @@ -923,7 +934,7 @@ class OAuthController { return publisher.notifySuccess(res, channel, providerConfigKey, connectionId); } - // check for oauth overrides in the connnection config + // check for oauth overrides in the connection config if (session.connectionConfig['oauth_client_id_override']) { config.oauth_client_id = session.connectionConfig['oauth_client_id_override']; } diff --git a/packages/types/lib/connect/api.ts b/packages/types/lib/connect/api.ts index 51c0a04fab..03381d0923 100644 --- a/packages/types/lib/connect/api.ts +++ b/packages/types/lib/connect/api.ts @@ -2,7 +2,18 @@ import type { Endpoint } from '../api.js'; export interface ConnectSessionPayload { allowed_integrations?: string[] | undefined; - integrations_config_defaults?: Record }> | undefined; + integrations_config_defaults?: + | Record< + string, + { + user_scopes?: string | undefined; + connection_config: { + [key: string]: unknown; + oauth_scopes_override?: string | undefined; + }; + } + > + | undefined; end_user: { id: string; email: string; diff --git a/packages/types/lib/connect/session.ts b/packages/types/lib/connect/session.ts index 6e1bb591a0..7d419a4099 100644 --- a/packages/types/lib/connect/session.ts +++ b/packages/types/lib/connect/session.ts @@ -4,7 +4,17 @@ export interface ConnectSession { readonly accountId: number; readonly environmentId: number; readonly allowedIntegrations?: string[] | null; - readonly integrationsConfigDefaults?: Record }> | null; + readonly integrationsConfigDefaults?: Record< + string, + { + /** Only used by Slack */ + user_scopes?: string | undefined; + connectionConfig: { + [key: string]: unknown; + oauth_scopes_override?: string | undefined; + }; + } + > | null; readonly createdAt: Date; readonly updatedAt: Date | null; } From 7b0a7c2719ecaaf5b3e6f71f57a0fbf19489c9fd Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 4 Dec 2024 10:39:20 +0100 Subject: [PATCH 10/45] feat(ui): new create connection page (#3072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 😘 Changes Fixes https://linear.app/nango/issue/NAN-2188/test-connection-ui - UI: New create connection page ~The goal is to give the opportunity for customers to change the `EndUser` (also education time) and display links to documentation. It's one more click but yeah.~ - UI: Keep the legacy page to a new endpoint for a moment - UI: add new style for Button, Input, Select https://www.figma.com/design/pMnSC7rhSM79Dqwhm7MFhd - `POST /api/v1/connect/sessions` now accepts the same payload as the public one - `GET /api/v1/meta` expose user UUID ~Not sure what the use of id but it's handy to have static-random id for the endUser profile without exposing our auto-incremented number.~ unused in the end ## 🧪 Tests - Go to UI > Connections > Create - Also go to Integrations > any > Add connection ![Screenshot 2024-12-02 at 13 17 52](https://github.com/user-attachments/assets/c2f9b096-1ecc-4deb-ad44-69fca96af931) --- package-lock.json | 198 ++ .../lib/controllers/connect/postSessions.ts | 2 +- .../connect/sessions/postConnectSessions.ts | 11 +- packages/types/lib/connect/api.ts | 2 +- packages/types/lib/integration/api.ts | 11 +- packages/webapp/package.json | 1 + packages/webapp/src/App.tsx | 5 +- packages/webapp/src/components/Info.tsx | 9 +- .../webapp/src/components/SimpleTooltip.tsx | 9 +- packages/webapp/src/components/ui/Alert.tsx | 6 +- .../webapp/src/components/ui/Collapsible.tsx | 9 + packages/webapp/src/components/ui/Command.tsx | 2 +- packages/webapp/src/components/ui/Select.tsx | 8 +- .../src/components/ui/button/Button.tsx | 10 +- .../webapp/src/components/ui/input/Input.tsx | 8 +- .../webapp/src/pages/Connection/Create.tsx | 1640 ++++------------- .../src/pages/Connection/CreateLegacy.tsx | 1307 +++++++++++++ packages/webapp/src/pages/Connection/List.tsx | 103 +- .../Endpoints/components/One.tsx | 2 +- .../Integrations/providerConfigKey/Show.tsx | 87 +- packages/webapp/tailwind.config.js | 5 +- 21 files changed, 1978 insertions(+), 1457 deletions(-) create mode 100644 packages/webapp/src/components/ui/Collapsible.tsx create mode 100644 packages/webapp/src/pages/Connection/CreateLegacy.tsx diff --git a/package-lock.json b/package-lock.json index f06f11366d..1c367bc3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6289,6 +6289,203 @@ } } }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", + "integrity": "sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==", + "dev": true, + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "dev": true + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "dev": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "dev": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dev": true, + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dev": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dev": true, + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dev": true, + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "dev": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dev": true, + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "dev": true, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -38582,6 +38779,7 @@ "@nangohq/server": "file:../server", "@nangohq/types": "file:../types", "@radix-ui/react-avatar": "1.1.1", + "@radix-ui/react-collapsible": "1.1.1", "@radix-ui/react-dialog": "1.1.1", "@radix-ui/react-dropdown-menu": "2.1.1", "@radix-ui/react-hover-card": "1.1.1", diff --git a/packages/server/lib/controllers/connect/postSessions.ts b/packages/server/lib/controllers/connect/postSessions.ts index f83b3490fb..5ccd24dd45 100644 --- a/packages/server/lib/controllers/connect/postSessions.ts +++ b/packages/server/lib/controllers/connect/postSessions.ts @@ -7,7 +7,7 @@ import * as endUserService from '@nangohq/shared'; import * as connectSessionService from '../../services/connectSession.service.js'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; -const bodySchema = z +export const bodySchema = z .object({ end_user: z .object({ diff --git a/packages/server/lib/controllers/v1/connect/sessions/postConnectSessions.ts b/packages/server/lib/controllers/v1/connect/sessions/postConnectSessions.ts index 45617f05e2..d302622798 100644 --- a/packages/server/lib/controllers/v1/connect/sessions/postConnectSessions.ts +++ b/packages/server/lib/controllers/v1/connect/sessions/postConnectSessions.ts @@ -1,12 +1,14 @@ import { asyncWrapper } from '../../../../utils/asyncWrapper.js'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import type { PostConnectSessions, PostInternalConnectSessions } from '@nangohq/types'; -import { postConnectSessions } from '../../../connect/postSessions.js'; +import { bodySchema as originalBodySchema, postConnectSessions } from '../../../connect/postSessions.js'; import { z } from 'zod'; const bodySchema = z .object({ - allowed_integrations: z.array(z.string()).optional() + allowed_integrations: originalBodySchema.shape.allowed_integrations, + end_user: originalBodySchema.shape.end_user, + organization: originalBodySchema.shape.organization }) .strict(); @@ -23,15 +25,14 @@ export const postInternalConnectSessions = asyncWrapper; + Body: Pick; }>; diff --git a/packages/types/lib/integration/api.ts b/packages/types/lib/integration/api.ts index 323429c124..e0611ce615 100644 --- a/packages/types/lib/integration/api.ts +++ b/packages/types/lib/integration/api.ts @@ -52,6 +52,16 @@ export type DeletePublicIntegration = Endpoint<{ Success: { success: true }; }>; +export type ApiIntegration = Omit, 'oauth_client_secret_iv' | 'oauth_client_secret_tag'>; + +export type GetIntegrations = Endpoint<{ + Method: 'GET'; + Path: '/api/v1/integrations'; + Success: { + data: ApiIntegration[]; + }; +}>; + export type PostIntegration = Endpoint<{ Method: 'POST'; Path: '/api/v1/integrations'; @@ -62,7 +72,6 @@ export type PostIntegration = Endpoint<{ }; }>; -export type ApiIntegration = Omit, 'oauth_client_secret_iv' | 'oauth_client_secret_tag'>; export type GetIntegration = Endpoint<{ Method: 'GET'; Path: '/api/v1/integrations/:providerConfigKey'; diff --git a/packages/webapp/package.json b/packages/webapp/package.json index e0b8b5548f..2ecc4d2222 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -39,6 +39,7 @@ "@nangohq/server": "file:../server", "@nangohq/types": "file:../types", "@radix-ui/react-avatar": "1.1.1", + "@radix-ui/react-collapsible": "1.1.1", "@radix-ui/react-dialog": "1.1.1", "@radix-ui/react-dropdown-menu": "2.1.1", "@radix-ui/react-hover-card": "1.1.1", diff --git a/packages/webapp/src/App.tsx b/packages/webapp/src/App.tsx index 22e86d4106..1c0fb957bc 100644 --- a/packages/webapp/src/App.tsx +++ b/packages/webapp/src/App.tsx @@ -18,7 +18,7 @@ import CreateIntegration from './pages/Integrations/Create'; import { ShowIntegration } from './pages/Integrations/providerConfigKey/Show'; import { ConnectionList } from './pages/Connection/List'; import { ConnectionShow } from './pages/Connection/Show'; -import ConnectionCreate from './pages/Connection/Create'; +import { ConnectionCreate } from './pages/Connection/Create'; import { EnvironmentSettings } from './pages/Environment/Settings'; import { PrivateRoute } from './components/PrivateRoute'; import ForgotPassword from './pages/Account/ForgotPassword'; @@ -35,6 +35,7 @@ import { TeamSettings } from './pages/Team/Settings'; import { UserSettings } from './pages/User/Settings'; import { Root } from './pages/Root'; import { globalEnv } from './utils/env'; +import { ConnectionCreateLegacy } from './pages/Connection/CreateLegacy'; import { Helmet } from 'react-helmet'; const theme = createTheme({ @@ -95,7 +96,7 @@ const App = () => { } /> } /> } /> - } /> + } /> } /> } /> } /> diff --git a/packages/webapp/src/components/Info.tsx b/packages/webapp/src/components/Info.tsx index a2f154ce93..05f0130d59 100644 --- a/packages/webapp/src/components/Info.tsx +++ b/packages/webapp/src/components/Info.tsx @@ -1,10 +1,9 @@ import type React from 'react'; import { Alert, AlertDescription, AlertTitle } from './ui/Alert'; -import { InfoCircledIcon } from '@radix-ui/react-icons'; import type { ComponentProps } from 'react'; import { cn } from '../utils/utils'; import Button from './ui/button/Button'; -import { IconX } from '@tabler/icons-react'; +import { IconInfoCircleFilled, IconX } from '@tabler/icons-react'; export const Info: React.FC<{ children: React.ReactNode; icon?: React.ReactNode; title?: string; onClose?: () => void } & ComponentProps> = ({ children, @@ -18,17 +17,17 @@ export const Info: React.FC<{ children: React.ReactNode; icon?: React.ReactNode; {icon ?? (
- +
)}
-
+
{title && {title}} {children}
diff --git a/packages/webapp/src/components/SimpleTooltip.tsx b/packages/webapp/src/components/SimpleTooltip.tsx index 397361f4e5..078e323625 100644 --- a/packages/webapp/src/components/SimpleTooltip.tsx +++ b/packages/webapp/src/components/SimpleTooltip.tsx @@ -1,7 +1,12 @@ import type React from 'react'; import { Tooltip, TooltipProvider, TooltipContent, TooltipTrigger } from './ui/Tooltip'; +import type { Content } from '@radix-ui/react-tooltip'; -export const SimpleTooltip: React.FC> = ({ tooltipContent, children }) => { +export const SimpleTooltip: React.FC>> = ({ + tooltipContent, + children, + ...rest +}) => { if (!tooltipContent) { return <>{children}; } @@ -9,7 +14,7 @@ export const SimpleTooltip: React.FC - {tooltipContent} + {tooltipContent} {children} diff --git a/packages/webapp/src/components/ui/Alert.tsx b/packages/webapp/src/components/ui/Alert.tsx index 226e96dd7f..24b6ed61f7 100644 --- a/packages/webapp/src/components/ui/Alert.tsx +++ b/packages/webapp/src/components/ui/Alert.tsx @@ -3,7 +3,7 @@ import { cva } from 'class-variance-authority'; import type { VariantProps } from 'class-variance-authority'; import { cn } from '../../utils/utils'; -const alertVariants = cva('relative w-full rounded-lg border px-3 py-1.5 text-sm flex gap-2 items-center flex items-center', { +const alertVariants = cva('relative w-full rounded-lg border px-2 py-2 text-sm flex gap-3 flex items-start', { variants: { variant: { default: 'bg-blue-base-35 border-blue-base text-blue-base', @@ -22,12 +22,12 @@ const Alert = React.forwardRef>(({ className, ...props }, ref) => ( -
+
)); AlertTitle.displayName = 'AlertTitle'; const AlertDescription = React.forwardRef>(({ className, ...props }, ref) => ( -
+
)); AlertDescription.displayName = 'AlertDescription'; diff --git a/packages/webapp/src/components/ui/Collapsible.tsx b/packages/webapp/src/components/ui/Collapsible.tsx new file mode 100644 index 0000000000..9605c4e41a --- /dev/null +++ b/packages/webapp/src/components/ui/Collapsible.tsx @@ -0,0 +1,9 @@ +import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'; + +const Collapsible = CollapsiblePrimitive.Root; + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger; + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent; + +export { Collapsible, CollapsibleTrigger, CollapsibleContent }; diff --git a/packages/webapp/src/components/ui/Command.tsx b/packages/webapp/src/components/ui/Command.tsx index 0117e87699..42ebbbb145 100644 --- a/packages/webapp/src/components/ui/Command.tsx +++ b/packages/webapp/src/components/ui/Command.tsx @@ -34,7 +34,7 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandInput = React.forwardRef, React.ComponentPropsWithoutRef>( ({ className, ...props }, ref) => ( // eslint-disable-next-line react/no-unknown-property -
+
span]:line-clamp-1', + 'transition-colors flex w-full items-center justify-between whitespace-nowrap rounded-md text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 ', + 'bg-grayscale-900 text-text-light-gray h-10 py-2 px-4', + 'hover:bg-grayscale-800 focus:bg-grayscale-800 data-[state="open"]:bg-grayscale-800', className )} {...props} @@ -54,7 +56,7 @@ const SelectContent = forwardRef, Rea >(({ className, type, before, after, inputSize, variant, ...props }, ref) => { return ( -
+
{before &&
{before}
} { + const toast = useToast(); const env = useStore((state) => state.env); - - const [loaded, setLoaded] = useState(false); - const [serverErrorMessage, setServerErrorMessage] = useState(''); - const [integrations, setIntegrations] = useState(null); + const paramExtended = useSearchParam('extended'); + const paramIntegrationId = useSearchParam('integration_id'); const navigate = useNavigate(); - const [integration, setIntegration] = useState(null); - const [connectionId, setConnectionId] = useState('test-connection-id'); - const [authMode, setAuthMode] = useState('OAUTH2'); - const [connectionConfigParams, setConnectionConfigParams] = useState | null>(null); - const [authorizationParams, setAuthorizationParams] = useState | null>(null); - const [authorizationParamsError, setAuthorizationParamsError] = useState(false); - const [selectedScopes, addToScopesSet, removeFromSelectedSet] = useSet(); - const [oauthSelectedScopes, oauthAddToScopesSet, oauthRemoveFromSelectedSet] = useSet(); - const [oauthccSelectedScopes, oauthccAddToScopesSet, oauthccRemoveFromSelectedSet] = useSet(); - const [publicKey, setPublicKey] = useState(''); - const [hostUrl, setHostUrl] = useState(''); - const [websocketsPath, setWebsocketsPath] = useState(''); - const [isHmacEnabled, setIsHmacEnabled] = useState(false); - const [hmacDigest, setHmacDigest] = useState(''); - const getIntegrationListAPI = useGetIntegrationListAPI(env); - const [apiKey, setApiKey] = useState(''); - const [apiAuthUsername, setApiAuthUsername] = useState(''); - const [apiAuthPassword, setApiAuthPassword] = useState(''); - const [oAuthClientId, setOAuthClientId] = useState(''); - const [tokenId, setTokenId] = useState(''); - const [tokenSecret, setTokenSecret] = useState(''); - const [patName, setpatName] = useState(''); - const [patSecret, setpatSecret] = useState(''); - const [contentUrl, setContentUrl] = useState(''); - const [organizationId, setOrganizationId] = useState(''); - const [devKey, setDevKey] = useState(''); - const [oAuthClientSecret, setOAuthClientSecret] = useState(''); - const [privateKeyId, setPrivateKeyId] = useState(''); - const [privateKey, setPrivateKey] = useState(''); - const [credentialsState, setCredentialsState] = useState>({}); - const [issuerId, setIssuerId] = useState(''); - const analyticsTrack = useAnalyticsTrack(); - const getHmacAPI = useGetHmacAPI(env); - const { providerConfigKey } = useParams(); - const { environmentAndAccount } = useEnvironment(env); - - useEffect(() => { - setLoaded(false); - }, [env]); - - useEffect(() => { - const getHmac = async () => { - const res = await getHmacAPI(integration?.uniqueKey as string, connectionId); + const { mutate, cache } = useSWRConfig(); - if (res?.status === 200) { - const hmacDigest = (await res.json())['hmac_digest']; - setHmacDigest(hmacDigest); - } - }; - if (isHmacEnabled && integration?.uniqueKey && connectionId) { - void getHmac(); - } - }, [isHmacEnabled, integration?.uniqueKey, connectionId]); - - useEffect(() => { - const getIntegrations = async () => { - const res = await getIntegrationListAPI(); - - if (res?.status === 200) { - const data = await res.json(); - setIntegrations(data['integrations']); - - if (data['integrations'] && data['integrations'].length > 0) { - const defaultIntegration = providerConfigKey - ? data['integrations'].find((i: Integration) => i.uniqueKey === providerConfigKey) - : data['integrations'][0]; - - setIntegration(defaultIntegration); - setUpConnectionConfigParams(defaultIntegration); - setAuthMode(defaultIntegration.authMode); - } - } - }; - - if (environmentAndAccount) { - const { environment, host } = environmentAndAccount; - setPublicKey(environment.public_key); - setHostUrl(host || baseUrl()); - setWebsocketsPath(environment.websockets_path || ''); - setIsHmacEnabled(Boolean(environment.hmac_key)); - } - - if (!loaded) { - setLoaded(true); - void getIntegrations(); - } - }, [loaded, setLoaded, setIntegrations, setIntegration, getIntegrationListAPI, environmentAndAccount, setPublicKey, providerConfigKey]); - - const handleCreate = (e: React.SyntheticEvent) => { - e.preventDefault(); - setServerErrorMessage(''); - - const target = e.target as typeof e.target & { - integration_unique_key: { value: string }; - connection_id: { value: string }; - connection_config_params: { value: string }; - user_scopes: { value: string }; - authorization_params: { value: string | undefined }; - }; - - const nango = new Nango({ host: hostUrl, websocketsPath, publicKey }); - - let credentials = {}; - let params = connectionConfigParams || {}; - - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - Object.keys(params).forEach((key) => params[key] === '' && delete params[key]); - - if (authMode === 'BASIC' || authMode === 'SIGNATURE') { - credentials = { - username: apiAuthUsername, - password: apiAuthPassword - }; - - if (authMode === 'SIGNATURE') { - credentials = { - ...credentials, - type: 'SIGNATURE' - }; - } - } + const connectUI = useRef(); + const hasConnected = useRef(); - if (authMode === 'API_KEY') { - credentials = { - apiKey - }; - } - - if (authMode === 'APP_STORE') { - credentials = { - privateKeyId, - issuerId, - privateKey - }; - } - - if (authMode === 'OAUTH2') { - credentials = { - oauth_client_id_override: oAuthClientId, - oauth_client_secret_override: oAuthClientSecret - }; - - if (oauthSelectedScopes.length > 0) { - params = { - ...params, - oauth_scopes_override: oauthSelectedScopes.join(',') - }; - } - } - - if (authMode === 'OAUTH2_CC') { - credentials = { - client_id: oAuthClientId, - client_secret: oAuthClientSecret - }; - - if (oauthccSelectedScopes.length > 0) { - params = { - ...params, - oauth_scopes: oauthccSelectedScopes.join(',') - }; - } - } - - if (authMode === 'TBA') { - credentials = { - oauth_client_id_override: oAuthClientId, - oauth_client_secret_override: oAuthClientSecret, - token_id: tokenId, - token_secret: tokenSecret - }; - if (oauthSelectedScopes.length > 0) { - params = { - ...params, - oauth_scopes_override: oauthSelectedScopes.join(',') - }; - } - } - - if (authMode === 'TABLEAU') { - credentials = { - pat_name: patName, - pat_secret: patSecret, - content_url: contentUrl - }; - } - - if (authMode === 'JWT') { - if (integration?.provider.includes('ghost-admin')) { - const privateKeyFormat = /^([^:]+):([^:]+)$/; - if (!privateKeyFormat.test(privateKey)) { - toast.error('The API key should be in the format id:secret.', { - position: toast.POSITION.BOTTOM_CENTER - }); - return; + const { environmentAndAccount } = useEnvironment(env); + const { user } = useUser(true); + const { list: listIntegration, mutate: listIntegrationMutate, loading } = useListIntegration(env); + + const [open, setOpen] = useState(false); + const [integration, setIntegration] = useState(); + const [testUserEmail, setTestUserEmail] = useState(user!.email); + const [testUserId, setTestUserId] = useState(`test_${user!.name.toLocaleLowerCase().replaceAll(' ', '_')}`); + const [testUserName, setTestUserName] = useState(user!.name); + const [testOrgName, setTestOrgName] = useState(''); + const [testOrgId, setTestOrgId] = useState(''); + + useUnmount(() => { + if (connectUI.current) { + connectUI.current.close(); + } + }); + + const onEvent: OnConnectEvent = useCallback( + (event) => { + if (event.type === 'close') { + void listIntegrationMutate(); + if (hasConnected.current) { + toast.toast({ title: `Connected to ${hasConnected.current.providerConfigKey}`, variant: 'success' }); + navigate(`/${env}/connections/${integration?.uniqueKey}/${hasConnected.current.connectionId}`); } - const [id, secret] = privateKey.split(':'); - credentials = { - privateKey: { id, secret } - }; - } else { - credentials = { - privateKeyId, - issuerId, - privateKey - }; + } else if (event.type === 'connect') { + void listIntegrationMutate(); + clearConnectionsCache(cache, mutate); + hasConnected.current = event.payload; } - } - - if (authMode === 'BILL') { - credentials = { - username: apiAuthUsername, - password: apiAuthPassword, - organization_id: organizationId, - dev_key: devKey - }; - } - if (authMode === 'TWO_STEP') { - credentials = { - type: 'TWO_STEP', - ...credentialsState - }; - } - const connectionConfig = { - user_scope: authMode === 'NONE' ? undefined : selectedScopes || [], - params, - authorization_params: authorizationParams || {}, - hmac: hmacDigest || '', - credentials - }; - const getConnection = - authMode === 'NONE' - ? nango.create(target.integration_unique_key.value, target.connection_id.value, connectionConfig) - : nango.auth(target.integration_unique_key.value, target.connection_id.value, connectionConfig); - - getConnection - .then(() => { - toast.success('Connection created!', { position: toast.POSITION.BOTTOM_CENTER }); - analyticsTrack('web:connection_created', { provider: integration?.provider || 'unknown' }); - void mutate((key) => typeof key === 'string' && key.startsWith('/api/v1/connections'), undefined); - navigate(`/${env}/connections`, { replace: true }); - }) - .catch((err: unknown) => { - setServerErrorMessage(err instanceof AuthError ? `${err.type} error: ${err.message}` : 'unknown error'); - }); - }; - - const setUpConnectionConfigParams = (integration: Integration) => { - if (integration == null) { - return; - } + }, + [toast] + ); - if (integration.connectionConfigParams == null || integration.connectionConfigParams.length === 0) { - setConnectionConfigParams(null); + const onClickConnectUI = () => { + if (!environmentAndAccount) { return; } - const params: Record = {}; - for (const key of Object.keys(integration.connectionConfigParams)) { - params[key] = ''; - } - setConnectionConfigParams(params); - }; - - const handleIntegrationUniqueKeyChange = (e: React.ChangeEvent) => { - const integration: Integration | undefined = integrations?.find((i) => i.uniqueKey === e.target.value); - - if (integration != null) { - setIntegration(integration); - setServerErrorMessage(''); - setUpConnectionConfigParams(integration); - setAuthMode(integration.authMode); - } - }; - - const handleConnectionIdChange = (e: React.ChangeEvent) => { - setConnectionId(e.target.value); - }; - - const handleConnectionConfigParamsChange = (e: React.ChangeEvent) => { - const params = connectionConfigParams ? Object.assign({}, connectionConfigParams) : {}; // Copy object to update UI. - params[e.target.name.replace('connection-config-', '')] = e.target.value; - setConnectionConfigParams(params); - }; - - const handleCredentialParamsChange = (paramName: string, value: string) => { - setCredentialsState((prevState) => ({ - ...prevState, - [paramName]: value - })); - }; - - const handleAuthorizationParamsChange = (e: React.ChangeEvent) => { - try { - setAuthorizationParams(JSON.parse(e.target.value)); - setAuthorizationParamsError(false); - } catch { - setAuthorizationParams(null); - setAuthorizationParamsError(true); - } - }; - - const snippet = () => { - const args = []; - - if (isStaging() || isHosted()) { - args.push(`host: '${hostUrl}'`); - if (websocketsPath && websocketsPath !== '/') { - args.push(`websocketsPath: '${websocketsPath}'`); - } - } - - if (publicKey) { - args.push(`publicKey: '${publicKey}'`); - } - - const argsStr = args.length > 0 ? `{ ${args.join(', ')} }` : ''; - - let connectionConfigParamsStr = ''; - - // Iterate of connection config params and create a string. - if (connectionConfigParams != null && Object.keys(connectionConfigParams).length >= 0) { - connectionConfigParamsStr = 'params: { '; - let hasAnyValue = false; - for (const [key, value] of Object.entries(connectionConfigParams)) { - if (value !== '') { - connectionConfigParamsStr += `${key}: '${value}', `; - hasAnyValue = true; - } - } - connectionConfigParamsStr = connectionConfigParamsStr.slice(0, -2); - connectionConfigParamsStr += ' }'; - if (!hasAnyValue) { - connectionConfigParamsStr = ''; - } - } - - if (authMode === 'OAUTH2' && oauthSelectedScopes.length > 0) { - if (connectionConfigParamsStr) { - connectionConfigParamsStr += ', '; - } else { - connectionConfigParamsStr = 'params: { '; - } - connectionConfigParamsStr += `oauth_scopes_override: '${oauthSelectedScopes.join(',')}', `; - connectionConfigParamsStr = connectionConfigParamsStr.slice(0, -2); - connectionConfigParamsStr += ' }'; - } - - let authorizationParamsStr = ''; - - // Iterate of authorization params and create a string. - if (authorizationParams != null && Object.keys(authorizationParams).length >= 0 && Object.keys(authorizationParams)[0]) { - authorizationParamsStr = 'authorization_params: { '; - for (const [key, value] of Object.entries(authorizationParams)) { - authorizationParamsStr += `${key}: '${value}', `; - } - authorizationParamsStr = authorizationParamsStr.slice(0, -2); - authorizationParamsStr += ' }'; - } - - let hmacKeyStr = ''; - - if (hmacDigest) { - hmacKeyStr = `hmac: '${hmacDigest}'`; - } - - let userScopesStr = ''; - - if (selectedScopes != null && selectedScopes.length > 0) { - userScopesStr = 'user_scope: [ '; - for (const scope of selectedScopes) { - userScopesStr += `'${scope}', `; - } - userScopesStr = userScopesStr.slice(0, -2); - userScopesStr += ' ]'; - } - - let apiAuthString = ''; - if (integration?.authMode === 'API_KEY') { - apiAuthString = ` - credentials: { - apiKey: '${apiKey}' - } - `; - } - - if (integration?.authMode === 'BASIC' || integration?.authMode === 'SIGNATURE') { - apiAuthString = ` - credentials: { - username: '${apiAuthUsername}', - password: '${apiAuthPassword}'${ - integration.authMode === 'SIGNATURE' - ? `, - Type: 'SIGNATURE'` - : '' - } - } - `; - } - - let appStoreAuthString = ''; - - if (integration?.authMode === 'APP_STORE') { - appStoreAuthString = ` - credentials: { - privateKeyId: '${privateKeyId}', - issuerId: '${issuerId}', - privateKey: '${privateKey}' - } - `; - } - - let oauthCredentialsString = ''; - - if (integration?.authMode === 'OAUTH2' && oAuthClientId && oAuthClientSecret) { - oauthCredentialsString = ` - credentials: { - oauth_client_id_override: '${oAuthClientId}', - oauth_client_secret_override: '${oAuthClientSecret}' - } - `; - } - let tbaCredentialsString = ''; - if (integration?.authMode === 'TBA') { - if (oAuthClientId && oAuthClientSecret) { - tbaCredentialsString = ` - credentials: { - token_id: '${tokenId}', - token_secret: '${tokenSecret}', - oauth_client_id_override: '${oAuthClientId}', - oauth_client_secret_override: '${oAuthClientSecret}' - } - `; - } else { - tbaCredentialsString = ` - credentials: { - token_id: '${tokenId}', - token_secret: '${tokenSecret}' - } - `; - } - } - - let tableauCredentialsString = ''; - if (integration?.authMode === 'TABLEAU') { - if (patName && patSecret && contentUrl) { - tableauCredentialsString = ` - credentials: { - pat_name: '${patName}', - pat_secret: '${patSecret}', - content_url: '${contentUrl}' - } - `; - } - } - - let jwtCredentialsString = ''; - - if (integration?.authMode === 'JWT') { - const credentials: string[] = []; - - if (integration.provider.includes('ghost-admin')) { - const [id = '', secret = ''] = privateKey.split(':'); - credentials.push(`privateKey: { id: '${id}', secret: '${secret}' }`); - } else { - if (privateKeyId) { - credentials.push(`privateKeyId: '${privateKeyId}'`); - } - if (issuerId) { - credentials.push(`issuerId: '${issuerId}'`); - } - if (privateKey) { - credentials.push(`privateKey: '${privateKey}'`); - } - } - - if (credentials.length > 0) { - jwtCredentialsString = ` - credentials: { - ${credentials.join(',\n ')} - } - `; + const nango = new Nango({ + host: environmentAndAccount.host, + websocketsPath: environmentAndAccount.environment.websockets_path || '' + }); + + connectUI.current = nango.openConnectUI({ + baseURL: globalEnv.connectUrl, + apiURL: globalEnv.apiUrl, + onEvent + }); + + // We defer the token creation so the iframe can open and display a loading screen + // instead of blocking the main loop and no visual clue for the end user + setTimeout(async () => { + const res = await apiConnectSessions(env, { + allowed_integrations: integration ? [integration.uniqueKey] : undefined, + end_user: { id: testUserId, email: testUserEmail, display_name: testUserName }, + organization: testOrgId ? { id: testOrgId, display_name: testOrgName } : undefined + }); + if ('error' in res.json) { + return; } - } + connectUI.current!.setSessionToken(res.json.data.token); + }, 10); + }; - let billCredentialsString = ''; - if (integration?.authMode === 'BILL') { - if (apiAuthUsername && apiAuthPassword && organizationId && devKey) { - billCredentialsString = ` - credentials: { - username: '${apiAuthUsername}', - password: '${apiAuthPassword}', - organization_id: '${organizationId}', - dev_key: '${devKey}' - } - `; + useEffect(() => { + if (paramIntegrationId && listIntegration?.integrations) { + const exists = listIntegration.integrations.find((v) => v.uniqueKey === paramIntegrationId); + if (exists) { + setIntegration(exists); } } - - let oauth2ClientCredentialsString = ''; - - if (integration?.authMode === 'OAUTH2_CC') { - if (oAuthClientId && oAuthClientSecret) { - oauth2ClientCredentialsString = ` - credentials: { - client_id: '${oAuthClientId}', - client_secret: '${oAuthClientSecret}' - } - `; - } - - if (oAuthClientId && !oAuthClientSecret) { - oauth2ClientCredentialsString = ` - credentials: { - client_id: '${oAuthClientId}' - } - `; - } - - if (!oAuthClientId && oAuthClientSecret) { - oauth2ClientCredentialsString = ` - credentials: { - client_secret: '${oAuthClientSecret}' + }, [paramIntegrationId, listIntegration]); + + if (loading) { + return ( + + + Create Connection - Nango + +
+
+

Create test connection

+
+ + + +
+
+
+
+ ); } - `; - } - - if (authMode === 'OAUTH2_CC' && oauthccSelectedScopes.length > 0) { - connectionConfigParamsStr = connectionConfigParamsStr ? `${connectionConfigParamsStr.slice(0, -2)}, ` : 'params: { '; - connectionConfigParamsStr += `oauth_scopes: '${oauthccSelectedScopes.join(',')}' }`; - } - } - - let twoStepCredentialsString = ''; - if (authMode === 'TWO_STEP') { - const credentialEntries = Object.entries(credentialsState); - - if (credentialEntries.length > 0) { - const credentialsString = credentialEntries.map(([key, value]) => `${key}: '${value}'`).join(',\n '); - - twoStepCredentialsString = ` - credentials: { - ${credentialsString} - } - `; - } - } - const connectionConfigStr = - !connectionConfigParamsStr && - !authorizationParamsStr && - !userScopesStr && - !hmacKeyStr && - !apiAuthString && - !appStoreAuthString && - !oauthCredentialsString && - !oauth2ClientCredentialsString && - !tableauCredentialsString && - !jwtCredentialsString && - !tbaCredentialsString && - !billCredentialsString && - !twoStepCredentialsString - ? '' - : ', { ' + - [ - connectionConfigParamsStr, - authorizationParamsStr, - hmacKeyStr, - userScopesStr, - apiAuthString, - appStoreAuthString, - oauthCredentialsString, - oauth2ClientCredentialsString, - tableauCredentialsString, - jwtCredentialsString, - tbaCredentialsString, - billCredentialsString, - twoStepCredentialsString - ] - .filter(Boolean) - .join(', ') + - '}'; - - return `import Nango from '@nangohq/frontend'; - -const nango = new Nango(${argsStr}); - -nango.${integration?.authMode === 'NONE' ? 'create' : 'auth'}('${integration?.uniqueKey}', '${connectionId}'${connectionConfigStr}) - .then((result: { providerConfigKey: string; connectionId: string }) => { - // do something - }).catch((err: { message: string; type: string }) => { - // handle error - });`; - }; return ( Create Connection - Nango - {integrations && !!integrations.length && publicKey && hostUrl && ( -
-

Add New Connection

-
-
-
-
-
- -
-
- -
-
-
-
- - -
-

{`The ID you will use to retrieve the connection (most often the user ID).`}

-
- - } +
+
+
+

Create a test connection

+
+ +
+ + +
-
- -
-
+ {integration ? ( +
+ {integration.uniqueKey} +
+ ) : ( + 'Choose from the list' + )} + + + + + + + + No framework found. + + {listIntegration?.integrations.map((item) => { + const checked = integration && item.uniqueKey === integration.uniqueKey; + return ( + { + setIntegration( + integration && curr === integration.uniqueKey + ? undefined + : listIntegration.integrations.find((v) => v.uniqueKey === curr)! + ); + setOpen(false); + }} + className={cn( + 'items-center pl-2 py-2.5 justify-between text-white', + checked && 'bg-grayscale-1000' + )} + > +
+ {item.uniqueKey} +
+ +
+ ); + })} +
+
+
+
+ + +
- {integration?.provider === 'slack' && ( -
-
- -
-
- null} - selectedScopes={selectedScopes} - addToScopesSet={addToScopesSet} - removeFromSelectedSet={removeFromSelectedSet} - minLength={1} - /> -
-
- )} +
- {authMode === 'OAUTH2_CC' && ( - <> -
-
- Client ID -
-
-
- + Test user email and name use your Nango account details. + + {paramExtended && ( + + + + + +
+
-
-
-
- Client Secret -
-
- -
-
-
-
- Scopes -
-
- null} - /> -
-
- - )} - - {integration?.provider.includes('netsuite') && ( -
-
- -
-
- setTestUserEmail(e.target.value)} />
-
- -
- {integration?.provider !== 'netsuite-tba' && ( - <> -
- -
-
- null} - selectedScopes={oauthSelectedScopes} - addToScopesSet={oauthAddToScopesSet} - removeFromSelectedSet={oauthRemoveFromSelectedSet} - minLength={1} - /> +
+
- )} - - {integration?.authMode === 'TBA' && ( -
-
- -
-
- -
-
- - -
-
- )} - - {integration?.authMode === 'TABLEAU' && ( -
-
- -
-
- -
-
- - -
-
- - -
-
- )} - {integration?.connectionConfigParams?.map((paramName: string) => ( -
-
- - -
-

{`Some integrations require extra configuration (cf.`}

- + Emulate your End User ID. In your production this would be your user's id. +
+ - docs -
-

{`).`}

-
- - } - > - -
-
-
- -
-
- ))} - - {(authMode === 'API_KEY' || authMode === 'BASIC' || authMode === 'BILL' || authMode === 'SIGNATURE') && ( -
-
- -

{authMode}

-
- - {(authMode === 'BASIC' || authMode === 'BILL' || authMode === 'SIGNATURE') && ( -
-
- -
- -
- -
- -
- -
- -
- -
-
- )} - {authMode === 'API_KEY' && ( -
-
- - -
-

{`The API key to authenticate requests`}

-
- - } - > - -
-
- -
- -
-
- )} -
- )} - - {integration?.authMode === 'BILL' && ( -
-
- -
-
- -
-
- - -
-
- )} - - {authMode === 'APP' && ( -
-
- - -
-

{`Add query parameters in the authorization URL, on a per-connection basis. Most integrations don't require this. This should be formatted as a JSON object, e.g. { "key" : "value" }. `}

-
- - } - > - -
-
-
- setTestUserId(e.target.value)} />
-
- )} - - {(authMode === 'APP_STORE' || authMode === 'JWT') && !integration?.provider.includes('ghost-admin') && ( -
-
- - -
-

{`Obtained after creating an API Key.`}

-
- - } - > - -
-
-
- setPrivateKeyId(e.target.value)} - /> -
-
- - -
-

{`is accessible in App Store Connect, under Users and Access, then Copy next to the ID`}

-
- - } - > - -
-
-
- setIssuerId(e.target.value)} - /> -
-
-
- )} - - {authMode === 'JWT' && integration?.provider.includes('ghost-admin') && ( -
-
-
- )} - - {(authMode === 'OAUTH1' || authMode === 'OAUTH2') && ( -
-
-
- )} - {authMode === 'TWO_STEP' && ( -
- {integration?.credentialParams?.map((paramName: string) => ( -
-
- -
- -
- handleCredentialParamsChange(paramName, value)} - /> -
-
- ))} -
- )} -
- {serverErrorMessage &&

{serverErrorMessage}

} -
- - -
-
-
- - {snippet()} - -
-
-
- + + + )} +
+ + + +
- )} - {integrations && !integrations.length && ( -
-
-

Add New Connection

-
-

- You have not created any Integrations yet. Please create an{' '} - - Integration - {' '} - first to create a Connection. Follow the{' '} - - Authorize an API guide - {' '} - for more instructions. -

-
+ - )} +
); -} +}; diff --git a/packages/webapp/src/pages/Connection/CreateLegacy.tsx b/packages/webapp/src/pages/Connection/CreateLegacy.tsx new file mode 100644 index 0000000000..39d6743641 --- /dev/null +++ b/packages/webapp/src/pages/Connection/CreateLegacy.tsx @@ -0,0 +1,1307 @@ +import { useNavigate, Link } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { useState, useEffect } from 'react'; +import { useSWRConfig } from 'swr'; +import Nango, { AuthError } from '@nangohq/frontend'; +import { Prism } from '@mantine/prism'; +import { HelpCircle } from '@geist-ui/icons'; +import { Tooltip } from '@geist-ui/core'; +import type { Integration } from '@nangohq/server'; + +import useSet from '../../hooks/useSet'; +import { isHosted, isStaging, baseUrl } from '../../utils/utils'; +import { useGetIntegrationListAPI, useGetHmacAPI } from '../../utils/api'; +import { useAnalyticsTrack } from '../../utils/analytics'; +import DashboardLayout from '../../layout/DashboardLayout'; +import TagsInput from '../../components/ui/input/TagsInput'; +import { LeftNavBarItems } from '../../components/LeftNavBar'; +import SecretInput from '../../components/ui/input/SecretInput'; +import SecretTextArea from '../../components/ui/input/SecretTextArea'; +import { useStore } from '../../store'; +import type { AuthModeType } from '@nangohq/types'; +import { useEnvironment } from '../../hooks/useEnvironment'; +import { Helmet } from 'react-helmet'; +import { useSearchParam } from 'react-use'; + +export const ConnectionCreateLegacy: React.FC = () => { + const { mutate } = useSWRConfig(); + const env = useStore((state) => state.env); + + const [loaded, setLoaded] = useState(false); + const [serverErrorMessage, setServerErrorMessage] = useState(''); + const [integrations, setIntegrations] = useState(null); + const navigate = useNavigate(); + const [integration, setIntegration] = useState(null); + const [connectionId, setConnectionId] = useState('test-connection-id'); + const [authMode, setAuthMode] = useState('OAUTH2'); + const [connectionConfigParams, setConnectionConfigParams] = useState | null>(null); + const [authorizationParams, setAuthorizationParams] = useState | null>(null); + const [authorizationParamsError, setAuthorizationParamsError] = useState(false); + const [selectedScopes, addToScopesSet, removeFromSelectedSet] = useSet(); + const [oauthSelectedScopes, oauthAddToScopesSet, oauthRemoveFromSelectedSet] = useSet(); + const [oauthccSelectedScopes, oauthccAddToScopesSet, oauthccRemoveFromSelectedSet] = useSet(); + const [publicKey, setPublicKey] = useState(''); + const [hostUrl, setHostUrl] = useState(''); + const [websocketsPath, setWebsocketsPath] = useState(''); + const [isHmacEnabled, setIsHmacEnabled] = useState(false); + const [hmacDigest, setHmacDigest] = useState(''); + const getIntegrationListAPI = useGetIntegrationListAPI(env); + const [apiKey, setApiKey] = useState(''); + const [apiAuthUsername, setApiAuthUsername] = useState(''); + const [apiAuthPassword, setApiAuthPassword] = useState(''); + const [oAuthClientId, setOAuthClientId] = useState(''); + const [tokenId, setTokenId] = useState(''); + const [tokenSecret, setTokenSecret] = useState(''); + const [patName, setpatName] = useState(''); + const [patSecret, setpatSecret] = useState(''); + const [contentUrl, setContentUrl] = useState(''); + const [organizationId, setOrganizationId] = useState(''); + const [devKey, setDevKey] = useState(''); + const [oAuthClientSecret, setOAuthClientSecret] = useState(''); + const [privateKeyId, setPrivateKeyId] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [credentialsState, setCredentialsState] = useState>({}); + const [issuerId, setIssuerId] = useState(''); + const analyticsTrack = useAnalyticsTrack(); + const getHmacAPI = useGetHmacAPI(env); + const providerConfigKey = useSearchParam('providerConfigKey'); + const { environmentAndAccount } = useEnvironment(env); + + useEffect(() => { + setLoaded(false); + }, [env]); + + useEffect(() => { + const getHmac = async () => { + const res = await getHmacAPI(integration?.uniqueKey as string, connectionId); + + if (res?.status === 200) { + const hmacDigest = (await res.json())['hmac_digest']; + setHmacDigest(hmacDigest); + } + }; + if (isHmacEnabled && integration?.uniqueKey && connectionId) { + void getHmac(); + } + }, [isHmacEnabled, integration?.uniqueKey, connectionId]); + + useEffect(() => { + const getIntegrations = async () => { + const res = await getIntegrationListAPI(); + + if (res?.status === 200) { + const data = await res.json(); + setIntegrations(data['integrations']); + + if (data['integrations'] && data['integrations'].length > 0) { + const defaultIntegration = providerConfigKey + ? data['integrations'].find((i: Integration) => i.uniqueKey === providerConfigKey) + : data['integrations'][0]; + + setIntegration(defaultIntegration); + setUpConnectionConfigParams(defaultIntegration); + setAuthMode(defaultIntegration.authMode); + } + } + }; + + if (environmentAndAccount) { + const { environment, host } = environmentAndAccount; + setPublicKey(environment.public_key); + setHostUrl(host || baseUrl()); + setWebsocketsPath(environment.websockets_path || ''); + setIsHmacEnabled(Boolean(environment.hmac_key)); + } + + if (!loaded) { + setLoaded(true); + void getIntegrations(); + } + }, [loaded, setLoaded, setIntegrations, setIntegration, getIntegrationListAPI, environmentAndAccount, setPublicKey, providerConfigKey]); + + const handleCreate = (e: React.SyntheticEvent) => { + e.preventDefault(); + setServerErrorMessage(''); + + const target = e.target as typeof e.target & { + integration_unique_key: { value: string }; + connection_id: { value: string }; + connection_config_params: { value: string }; + user_scopes: { value: string }; + authorization_params: { value: string | undefined }; + }; + + const nango = new Nango({ host: hostUrl, websocketsPath, publicKey }); + + let credentials = {}; + let params = connectionConfigParams || {}; + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + Object.keys(params).forEach((key) => params[key] === '' && delete params[key]); + + if (authMode === 'BASIC' || authMode === 'SIGNATURE') { + credentials = { + username: apiAuthUsername, + password: apiAuthPassword + }; + + if (authMode === 'SIGNATURE') { + credentials = { + ...credentials, + type: 'SIGNATURE' + }; + } + } + + if (authMode === 'API_KEY') { + credentials = { + apiKey + }; + } + + if (authMode === 'APP_STORE') { + credentials = { + privateKeyId, + issuerId, + privateKey + }; + } + + if (authMode === 'OAUTH2') { + credentials = { + oauth_client_id_override: oAuthClientId, + oauth_client_secret_override: oAuthClientSecret + }; + + if (oauthSelectedScopes.length > 0) { + params = { + ...params, + oauth_scopes_override: oauthSelectedScopes.join(',') + }; + } + } + + if (authMode === 'OAUTH2_CC') { + credentials = { + client_id: oAuthClientId, + client_secret: oAuthClientSecret + }; + + if (oauthccSelectedScopes.length > 0) { + params = { + ...params, + oauth_scopes: oauthccSelectedScopes.join(',') + }; + } + } + + if (authMode === 'TBA') { + credentials = { + oauth_client_id_override: oAuthClientId, + oauth_client_secret_override: oAuthClientSecret, + token_id: tokenId, + token_secret: tokenSecret + }; + if (oauthSelectedScopes.length > 0) { + params = { + ...params, + oauth_scopes_override: oauthSelectedScopes.join(',') + }; + } + } + + if (authMode === 'TABLEAU') { + credentials = { + pat_name: patName, + pat_secret: patSecret, + content_url: contentUrl + }; + } + + if (authMode === 'JWT') { + if (integration?.provider.includes('ghost-admin')) { + const privateKeyFormat = /^([^:]+):([^:]+)$/; + if (!privateKeyFormat.test(privateKey)) { + toast.error('The API key should be in the format id:secret.', { + position: toast.POSITION.BOTTOM_CENTER + }); + return; + } + const [id, secret] = privateKey.split(':'); + credentials = { + privateKey: { id, secret } + }; + } else { + credentials = { + privateKeyId, + issuerId, + privateKey + }; + } + } + + if (authMode === 'BILL') { + credentials = { + username: apiAuthUsername, + password: apiAuthPassword, + organization_id: organizationId, + dev_key: devKey + }; + } + if (authMode === 'TWO_STEP') { + credentials = { + type: 'TWO_STEP', + ...credentialsState + }; + } + const connectionConfig = { + user_scope: authMode === 'NONE' ? undefined : selectedScopes || [], + params, + authorization_params: authorizationParams || {}, + hmac: hmacDigest || '', + credentials + }; + const getConnection = + authMode === 'NONE' + ? nango.create(target.integration_unique_key.value, target.connection_id.value, connectionConfig) + : nango.auth(target.integration_unique_key.value, target.connection_id.value, connectionConfig); + + getConnection + .then(() => { + toast.success('Connection created!', { position: toast.POSITION.BOTTOM_CENTER }); + analyticsTrack('web:connection_created', { provider: integration?.provider || 'unknown' }); + void mutate((key) => typeof key === 'string' && key.startsWith('/api/v1/connections'), undefined); + navigate(`/${env}/connections`, { replace: true }); + }) + .catch((err: unknown) => { + setServerErrorMessage(err instanceof AuthError ? `${err.type} error: ${err.message}` : 'unknown error'); + }); + }; + + const setUpConnectionConfigParams = (integration: Integration) => { + if (integration == null) { + return; + } + + if (integration.connectionConfigParams == null || integration.connectionConfigParams.length === 0) { + setConnectionConfigParams(null); + return; + } + + const params: Record = {}; + for (const key of Object.keys(integration.connectionConfigParams)) { + params[key] = ''; + } + setConnectionConfigParams(params); + }; + + const handleIntegrationUniqueKeyChange = (e: React.ChangeEvent) => { + const integration: Integration | undefined = integrations?.find((i) => i.uniqueKey === e.target.value); + + if (integration != null) { + setIntegration(integration); + setServerErrorMessage(''); + setUpConnectionConfigParams(integration); + setAuthMode(integration.authMode); + } + }; + + const handleConnectionIdChange = (e: React.ChangeEvent) => { + setConnectionId(e.target.value); + }; + + const handleConnectionConfigParamsChange = (e: React.ChangeEvent) => { + const params = connectionConfigParams ? Object.assign({}, connectionConfigParams) : {}; // Copy object to update UI. + params[e.target.name.replace('connection-config-', '')] = e.target.value; + setConnectionConfigParams(params); + }; + + const handleCredentialParamsChange = (paramName: string, value: string) => { + setCredentialsState((prevState) => ({ + ...prevState, + [paramName]: value + })); + }; + + const handleAuthorizationParamsChange = (e: React.ChangeEvent) => { + try { + setAuthorizationParams(JSON.parse(e.target.value)); + setAuthorizationParamsError(false); + } catch { + setAuthorizationParams(null); + setAuthorizationParamsError(true); + } + }; + + const snippet = () => { + const args = []; + + if (isStaging() || isHosted()) { + args.push(`host: '${hostUrl}'`); + if (websocketsPath && websocketsPath !== '/') { + args.push(`websocketsPath: '${websocketsPath}'`); + } + } + + if (publicKey) { + args.push(`publicKey: '${publicKey}'`); + } + + const argsStr = args.length > 0 ? `{ ${args.join(', ')} }` : ''; + + let connectionConfigParamsStr = ''; + + // Iterate of connection config params and create a string. + if (connectionConfigParams != null && Object.keys(connectionConfigParams).length >= 0) { + connectionConfigParamsStr = 'params: { '; + let hasAnyValue = false; + for (const [key, value] of Object.entries(connectionConfigParams)) { + if (value !== '') { + connectionConfigParamsStr += `${key}: '${value}', `; + hasAnyValue = true; + } + } + connectionConfigParamsStr = connectionConfigParamsStr.slice(0, -2); + connectionConfigParamsStr += ' }'; + if (!hasAnyValue) { + connectionConfigParamsStr = ''; + } + } + + if (authMode === 'OAUTH2' && oauthSelectedScopes.length > 0) { + if (connectionConfigParamsStr) { + connectionConfigParamsStr += ', '; + } else { + connectionConfigParamsStr = 'params: { '; + } + connectionConfigParamsStr += `oauth_scopes_override: '${oauthSelectedScopes.join(',')}', `; + connectionConfigParamsStr = connectionConfigParamsStr.slice(0, -2); + connectionConfigParamsStr += ' }'; + } + + let authorizationParamsStr = ''; + + // Iterate of authorization params and create a string. + if (authorizationParams != null && Object.keys(authorizationParams).length >= 0 && Object.keys(authorizationParams)[0]) { + authorizationParamsStr = 'authorization_params: { '; + for (const [key, value] of Object.entries(authorizationParams)) { + authorizationParamsStr += `${key}: '${value}', `; + } + authorizationParamsStr = authorizationParamsStr.slice(0, -2); + authorizationParamsStr += ' }'; + } + + let hmacKeyStr = ''; + + if (hmacDigest) { + hmacKeyStr = `hmac: '${hmacDigest}'`; + } + + let userScopesStr = ''; + + if (selectedScopes != null && selectedScopes.length > 0) { + userScopesStr = 'user_scope: [ '; + for (const scope of selectedScopes) { + userScopesStr += `'${scope}', `; + } + userScopesStr = userScopesStr.slice(0, -2); + userScopesStr += ' ]'; + } + + let apiAuthString = ''; + if (integration?.authMode === 'API_KEY') { + apiAuthString = ` + credentials: { + apiKey: '${apiKey}' + } + `; + } + + if (integration?.authMode === 'BASIC' || integration?.authMode === 'SIGNATURE') { + apiAuthString = ` + credentials: { + username: '${apiAuthUsername}', + password: '${apiAuthPassword}'${ + integration.authMode === 'SIGNATURE' + ? `, + Type: 'SIGNATURE'` + : '' + } + } + `; + } + + let appStoreAuthString = ''; + + if (integration?.authMode === 'APP_STORE') { + appStoreAuthString = ` + credentials: { + privateKeyId: '${privateKeyId}', + issuerId: '${issuerId}', + privateKey: '${privateKey}' + } + `; + } + + let oauthCredentialsString = ''; + + if (integration?.authMode === 'OAUTH2' && oAuthClientId && oAuthClientSecret) { + oauthCredentialsString = ` + credentials: { + oauth_client_id_override: '${oAuthClientId}', + oauth_client_secret_override: '${oAuthClientSecret}' + } + `; + } + let tbaCredentialsString = ''; + if (integration?.authMode === 'TBA') { + if (oAuthClientId && oAuthClientSecret) { + tbaCredentialsString = ` + credentials: { + token_id: '${tokenId}', + token_secret: '${tokenSecret}', + oauth_client_id_override: '${oAuthClientId}', + oauth_client_secret_override: '${oAuthClientSecret}' + } + `; + } else { + tbaCredentialsString = ` + credentials: { + token_id: '${tokenId}', + token_secret: '${tokenSecret}' + } + `; + } + } + + let tableauCredentialsString = ''; + if (integration?.authMode === 'TABLEAU') { + if (patName && patSecret && contentUrl) { + tableauCredentialsString = ` + credentials: { + pat_name: '${patName}', + pat_secret: '${patSecret}', + content_url: '${contentUrl}' + } + `; + } + } + + let jwtCredentialsString = ''; + + if (integration?.authMode === 'JWT') { + const credentials: string[] = []; + + if (integration.provider.includes('ghost-admin')) { + const [id = '', secret = ''] = privateKey.split(':'); + credentials.push(`privateKey: { id: '${id}', secret: '${secret}' }`); + } else { + if (privateKeyId) { + credentials.push(`privateKeyId: '${privateKeyId}'`); + } + if (issuerId) { + credentials.push(`issuerId: '${issuerId}'`); + } + if (privateKey) { + credentials.push(`privateKey: '${privateKey}'`); + } + } + + if (credentials.length > 0) { + jwtCredentialsString = ` + credentials: { + ${credentials.join(',\n ')} + } + `; + } + } + + let billCredentialsString = ''; + if (integration?.authMode === 'BILL') { + if (apiAuthUsername && apiAuthPassword && organizationId && devKey) { + billCredentialsString = ` + credentials: { + username: '${apiAuthUsername}', + password: '${apiAuthPassword}', + organization_id: '${organizationId}', + dev_key: '${devKey}' + } + `; + } + } + + let oauth2ClientCredentialsString = ''; + + if (integration?.authMode === 'OAUTH2_CC') { + if (oAuthClientId && oAuthClientSecret) { + oauth2ClientCredentialsString = ` + credentials: { + client_id: '${oAuthClientId}', + client_secret: '${oAuthClientSecret}' + } + `; + } + + if (oAuthClientId && !oAuthClientSecret) { + oauth2ClientCredentialsString = ` + credentials: { + client_id: '${oAuthClientId}' + } + `; + } + + if (!oAuthClientId && oAuthClientSecret) { + oauth2ClientCredentialsString = ` + credentials: { + client_secret: '${oAuthClientSecret}' + } + `; + } + + if (authMode === 'OAUTH2_CC' && oauthccSelectedScopes.length > 0) { + connectionConfigParamsStr = connectionConfigParamsStr ? `${connectionConfigParamsStr.slice(0, -2)}, ` : 'params: { '; + connectionConfigParamsStr += `oauth_scopes: '${oauthccSelectedScopes.join(',')}' }`; + } + } + + let twoStepCredentialsString = ''; + if (authMode === 'TWO_STEP') { + const credentialEntries = Object.entries(credentialsState); + + if (credentialEntries.length > 0) { + const credentialsString = credentialEntries.map(([key, value]) => `${key}: '${value}'`).join(',\n '); + + twoStepCredentialsString = ` + credentials: { + ${credentialsString} + } + `; + } + } + const connectionConfigStr = + !connectionConfigParamsStr && + !authorizationParamsStr && + !userScopesStr && + !hmacKeyStr && + !apiAuthString && + !appStoreAuthString && + !oauthCredentialsString && + !oauth2ClientCredentialsString && + !tableauCredentialsString && + !jwtCredentialsString && + !tbaCredentialsString && + !billCredentialsString && + !twoStepCredentialsString + ? '' + : ', { ' + + [ + connectionConfigParamsStr, + authorizationParamsStr, + hmacKeyStr, + userScopesStr, + apiAuthString, + appStoreAuthString, + oauthCredentialsString, + oauth2ClientCredentialsString, + tableauCredentialsString, + jwtCredentialsString, + tbaCredentialsString, + billCredentialsString, + twoStepCredentialsString + ] + .filter(Boolean) + .join(', ') + + '}'; + + return `import Nango from '@nangohq/frontend'; + +const nango = new Nango(${argsStr}); + +nango.${integration?.authMode === 'NONE' ? 'create' : 'auth'}('${integration?.uniqueKey}', '${connectionId}'${connectionConfigStr}) + .then((result: { providerConfigKey: string; connectionId: string }) => { + // do something + }).catch((err: { message: string; type: string }) => { + // handle error + });`; + }; + + return ( + + + Create Connection - Nango + + {integrations && !!integrations.length && publicKey && hostUrl && ( +
+

Add New Connection

+
+
+
+
+
+ +
+
+ +
+
+
+
+ + +
+

{`The ID you will use to retrieve the connection (most often the user ID).`}

+
+ + } + > + +
+
+
+ +
+
+
+ {integration?.provider === 'slack' && ( +
+
+ +
+
+ null} + selectedScopes={selectedScopes} + addToScopesSet={addToScopesSet} + removeFromSelectedSet={removeFromSelectedSet} + minLength={1} + /> +
+
+ )} + + {authMode === 'OAUTH2_CC' && ( + <> +
+
+ Client ID +
+
+
+ +
+
+
+
+
+ Client Secret +
+
+ +
+
+
+
+ Scopes +
+
+ null} + /> +
+
+ + )} + + {integration?.provider.includes('netsuite') && ( +
+
+ +
+
+ +
+
+ +
+ {integration?.provider !== 'netsuite-tba' && ( + <> +
+ +
+
+ null} + selectedScopes={oauthSelectedScopes} + addToScopesSet={oauthAddToScopesSet} + removeFromSelectedSet={oauthRemoveFromSelectedSet} + minLength={1} + /> +
+ + )} +
+ )} + + {integration?.authMode === 'TBA' && ( +
+
+ +
+
+ +
+
+ + +
+
+ )} + + {integration?.authMode === 'TABLEAU' && ( +
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+ )} + {integration?.connectionConfigParams?.map((paramName: string) => ( +
+
+ + +
+

{`Some integrations require extra configuration (cf.`}

+ + docs + +

{`).`}

+
+ + } + > + +
+
+
+ +
+
+ ))} + + {(authMode === 'API_KEY' || authMode === 'BASIC' || authMode === 'BILL' || authMode === 'SIGNATURE') && ( +
+
+ +

{authMode}

+
+ + {(authMode === 'BASIC' || authMode === 'BILL' || authMode === 'SIGNATURE') && ( +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ )} + {authMode === 'API_KEY' && ( +
+
+ + +
+

{`The API key to authenticate requests`}

+
+ + } + > + +
+
+ +
+ +
+
+ )} +
+ )} + + {integration?.authMode === 'BILL' && ( +
+
+ +
+
+ +
+
+ + +
+
+ )} + + {authMode === 'APP' && ( +
+
+ + +
+

{`Add query parameters in the authorization URL, on a per-connection basis. Most integrations don't require this. This should be formatted as a JSON object, e.g. { "key" : "value" }. `}

+
+ + } + > + +
+
+
+ +
+
+ )} + + {(authMode === 'APP_STORE' || authMode === 'JWT') && !integration?.provider.includes('ghost-admin') && ( +
+
+ + +
+

{`Obtained after creating an API Key.`}

+
+ + } + > + +
+
+
+ setPrivateKeyId(e.target.value)} + /> +
+
+ + +
+

{`is accessible in App Store Connect, under Users and Access, then Copy next to the ID`}

+
+ + } + > + +
+
+
+ setIssuerId(e.target.value)} + /> +
+
+ + +
+

{`Obtained after creating an API Key. This value should be base64 encoded when passing to the auth call`}

+
+ + } + > + +
+
+ +
+ setPrivateKey(value)} + required + /> +
+
+ )} + + {authMode === 'JWT' && integration?.provider.includes('ghost-admin') && ( +
+
+ +
+ +
+ +
+
+ )} + + {(authMode === 'OAUTH1' || authMode === 'OAUTH2') && ( +
+
+ + +
+

{`Add query parameters in the authorization URL, on a per-connection basis. Most integrations don't require this. This should be formatted as a JSON object, e.g. { "key" : "value" }. `}

+
+ + } + > + +
+
+
+ +
+
+ )} + {authMode === 'TWO_STEP' && ( +
+ {integration?.credentialParams?.map((paramName: string) => ( +
+
+ +
+ +
+ handleCredentialParamsChange(paramName, value)} + /> +
+
+ ))} +
+ )} +
+ {serverErrorMessage &&

{serverErrorMessage}

} +
+ + +
+
+
+ + {snippet()} + +
+
+
+
+
+
+ )} + {integrations && !integrations.length && ( +
+
+

Add New Connection

+
+

+ You have not created any Integrations yet. Please create an{' '} + + Integration + {' '} + first to create a Connection. Follow the{' '} + + Authorize an API guide + {' '} + for more instructions. +

+
+
+
+ )} +
+ ); +}; diff --git a/packages/webapp/src/pages/Connection/List.tsx b/packages/webapp/src/pages/Connection/List.tsx index 89f1697c43..bf466cea28 100644 --- a/packages/webapp/src/pages/Connection/List.tsx +++ b/packages/webapp/src/pages/Connection/List.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { useState, useRef, useMemo, useEffect, useCallback } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Link } from 'react-router-dom'; import * as Table from '../../components/ui/Table'; @@ -12,13 +12,8 @@ import { MultiSelect } from '../../components/MultiSelect'; import { useStore } from '../../store'; import Button from '../../components/ui/button/Button'; -import { useEnvironment } from '../../hooks/useEnvironment'; -import { baseUrl, formatDateToInternationalFormat } from '../../utils/utils'; -import type { AuthResult, ConnectUI, OnConnectEvent } from '@nangohq/frontend'; -import Nango from '@nangohq/frontend'; -import { useDebounce, useUnmount } from 'react-use'; -import { globalEnv } from '../../utils/env'; -import { apiConnectSessions } from '../../hooks/useConnect'; +import { formatDateToInternationalFormat } from '../../utils/utils'; +import { useDebounce } from 'react-use'; import { useListIntegration } from '../../hooks/useIntegration'; import { Skeleton } from '../../components/ui/Skeleton'; import type { ColumnDef } from '@tanstack/react-table'; @@ -27,9 +22,6 @@ import IntegrationLogo from '../../components/ui/IntegrationLogo'; import { ErrorCircle } from '../../components/ErrorCircle'; import Spinner from '../../components/ui/Spinner'; import { AvatarOrganization } from '../../components/AvatarCustom'; -import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '../../components/ui/DropdownMenu'; -import { IconChevronDown } from '@tabler/icons-react'; -import { useToast } from '../../hooks/useToast'; import type { ApiConnectionSimple } from '@nangohq/types'; import { CopyText } from '../../components/CopyText'; import { SimpleTooltip } from '../../components/SimpleTooltip'; @@ -132,14 +124,9 @@ const columns: ColumnDef[] = [ ]; export const ConnectionList: React.FC = () => { - const toast = useToast(); const env = useStore((state) => state.env); - const connectUI = useRef(); - const hasConnected = useRef(); - - const { environmentAndAccount } = useEnvironment(env); - const { list: listIntegration, mutate: listIntegrationMutate } = useListIntegration(env); + const { list: listIntegration } = useListIntegration(env); const { data: connectionsCount } = useConnectionsCount(env); const [selectedIntegration, setSelectedIntegration] = useState(defaultFilter); @@ -148,19 +135,13 @@ export const ConnectionList: React.FC = () => { const [filterWithError, setFilterWithError] = useState('all'); const [readyToDisplay, setReadyToDisplay] = useState(false); - const { data, loading, error, hasNext, offset, setOffset, mutate } = useConnections({ + const { data, loading, error, hasNext, offset, setOffset } = useConnections({ env, search: debouncedSearch, integrationIds: selectedIntegration, withError: filterWithError === 'all' ? undefined : filterWithError === 'error' }); - useUnmount(() => { - if (connectUI.current) { - connectUI.current.close(); - } - }); - useDebounce(() => setDebouncedSearch(search), 250, [search]); const handleInputChange = (event: React.ChangeEvent | React.KeyboardEvent) => { @@ -180,50 +161,6 @@ export const ConnectionList: React.FC = () => { setFilterWithError(newItems.length > 0 ? newItems[0] : defaultFilter[0]); }; - const onEvent: OnConnectEvent = useCallback( - (event) => { - if (event.type === 'close') { - void mutate(); - void listIntegrationMutate(); - if (hasConnected.current) { - toast.toast({ title: `Connected to ${hasConnected.current.providerConfigKey}`, variant: 'success' }); - } - } else if (event.type === 'connect') { - void mutate(); - void listIntegrationMutate(); - hasConnected.current = event.payload; - } - }, - [toast] - ); - - const onClickConnectUI = () => { - if (!environmentAndAccount) { - return; - } - - const nango = new Nango({ - host: environmentAndAccount.host || baseUrl(), - websocketsPath: environmentAndAccount.environment.websockets_path || '' - }); - - connectUI.current = nango.openConnectUI({ - baseURL: globalEnv.connectUrl, - apiURL: globalEnv.apiUrl, - onEvent - }); - - // We defer the token creation so the iframe can open and display a loading screen - // instead of blocking the main loop and no visual clue for the end user - setTimeout(async () => { - const res = await apiConnectSessions(env, {}); - if ('error' in res.json) { - return; - } - connectUI.current!.setSessionToken(res.json.data.token); - }, 10); - }; - const integrations = useMemo(() => { if (!listIntegration) { return []; @@ -282,26 +219,12 @@ export const ConnectionList: React.FC = () => {

Connections

-
- - - - - - - - - - - - - -
+
{connections && (connections.length > 0 || hasFiltered) && ( @@ -441,10 +364,12 @@ export const ConnectionList: React.FC = () => { , or manually here.
- + + +
)} diff --git a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/One.tsx b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/One.tsx index 8a613cbfb4..a0b5d5e7e9 100644 --- a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/One.tsx +++ b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/One.tsx @@ -216,7 +216,7 @@ export const EndpointOne: React.FC<{ integration: GetIntegration['Success']['dat
Request