-
Notifications
You must be signed in to change notification settings - Fork 0
oauth sample app #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
cb-jananivijayan
wants to merge
1
commit into
main
Choose a base branch
from
oauth-app
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
oauth sample app #22
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| oauth.local.json | ||
| .oauth.key | ||
| iparams.local.json | ||
| logs/ | ||
| dist/ | ||
| *.zip |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # oauth-crm-sync | ||
|
|
||
| Sample Chargebee custom app demonstrating OAuth 2.0 integration. | ||
|
|
||
| Listens to `customer_created` and `subscription_created` events, then syncs the data to HubSpot CRM using an OAuth-authorized access token. | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Create a HubSpot OAuth app | ||
|
|
||
| 1. Go to [HubSpot Developer Portal](https://developers.hubspot.com/) → Apps → Create app | ||
| 2. Under **Auth** → **OAuth**, note your **Client ID** and **Client Secret** | ||
| 3. Add redirect URI: `http://localhost:10101/oauth/callback` (or the port you run the CLI on) | ||
| 4. Add scopes: `crm.objects.contacts.read crm.objects.contacts.write` | ||
|
|
||
| ### 2. Configure credentials | ||
|
|
||
| Edit `oauth_config.json` and replace the placeholder values: | ||
|
|
||
| ```json | ||
| { | ||
| "connectors": { | ||
| "hubspot": { | ||
| "client_id": "<YOUR_HUBSPOT_CLIENT_ID>", | ||
| "client_secret": "<YOUR_HUBSPOT_CLIENT_SECRET>", | ||
| ... | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### 3. Run the tester UI | ||
|
|
||
| ```bash | ||
| cb-apps run --dir . | ||
| ``` | ||
|
|
||
| Open `http://localhost:10101` in your browser. | ||
|
|
||
| ### 4. Authorize via OAuth | ||
|
|
||
| 1. Click the **OAuth** tab in the tester UI | ||
| 2. Click **Connect** next to `hubspot` | ||
| 3. Complete the HubSpot authorization flow in the popup | ||
| 4. The tab shows **Authorized** once the token is saved | ||
|
|
||
| ### 5. Test an event | ||
|
|
||
| 1. Select `customer_created` from the event dropdown | ||
| 2. Click **Invoke** — the handler creates or updates a HubSpot contact | ||
| 3. Select `subscription_created` and click **Invoke** — a note is added in HubSpot | ||
|
|
||
| ## How OAuth tokens work | ||
|
|
||
| - Tokens are encrypted with AES-256-GCM and stored in `oauth.local.json` (gitignored) | ||
| - The encryption key lives in `.oauth.key` (also gitignored, `0600` permissions) | ||
| - On every invocation, the CLI decrypts the token and injects it as: | ||
| ```js | ||
| payload.oauth_token.hubspot.access_token // Bearer token | ||
| payload.oauth_token.hubspot.token_type // "Bearer" | ||
| ``` | ||
| - Only `access_token` and `token_type` are exposed to handler code; `refresh_token` stays encrypted on disk | ||
|
|
||
| ## Files | ||
|
|
||
| | File | Purpose | | ||
| |------|---------| | ||
| | `manifest.json` | App metadata and event-to-handler mapping | | ||
| | `oauth_config.json` | OAuth connector credentials (fill in your client_id/secret) | | ||
| | `handler/handler.js` | Event handler code | | ||
| | `test_data/` | Sample payloads for local testing | | ||
| | `types/types.d.ts` | TypeScript type hints for the handler payload | | ||
| | `.gitignore` | Excludes secrets and generated files | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /** | ||
| * oauth-crm-sync — sample app demonstrating OAuth 2.0 token usage. | ||
| * | ||
| * How OAuth tokens reach this handler: | ||
| * 1. Add your OAuth credentials to oauth_config.json. | ||
| * 2. Open the tester UI (cb-apps run), click the OAuth tab, and click Connect. | ||
| * 3. After authorizing, tokens are encrypted and saved locally. | ||
| * 4. On every invocation, the CLI decrypts the token and injects it as | ||
| * payload.oauth_token["hubspot"].access_token (Bearer token ready to use). | ||
| * | ||
| * Available in payload.oauth_token only when: | ||
| * - oauth_config.json exists in the app directory, AND | ||
| * - the connector has been authorized via the tester UI | ||
| */ | ||
|
|
||
| 'use strict'; | ||
|
|
||
| const HUBSPOT_API = 'https://api.hubapi.com'; | ||
|
|
||
| module.exports = { | ||
| /** | ||
| * Creates or updates a HubSpot contact when a Chargebee customer is created. | ||
| * @param {import('../types/types.d.ts').HandlerPayload} payload | ||
| */ | ||
| onCustomerCreated: async function (payload) { | ||
| const token = getToken(payload, 'hubspot'); | ||
| const customer = payload.event.content.customer; | ||
|
|
||
| const [firstName, ...rest] = (customer.first_name || '').split(' '); | ||
| const lastName = customer.last_name || rest.join(' ') || ''; | ||
|
|
||
| const contact = { | ||
| properties: { | ||
| email: customer.email, | ||
| firstname: firstName, | ||
| lastname: lastName, | ||
| phone: customer.phone || '', | ||
| company: customer.company || '', | ||
| chargebee_customer_id: customer.id, | ||
| }, | ||
| }; | ||
|
|
||
| const existing = await hubspotGet(token, `/crm/v3/objects/contacts/${customer.email}?idProperty=email`); | ||
|
|
||
| if (existing.id) { | ||
| await hubspotPatch(token, `/crm/v3/objects/contacts/${existing.id}`, contact); | ||
| console.log(`[CRM Sync] Updated HubSpot contact ${existing.id} for customer ${customer.id}`); | ||
| } else { | ||
| const created = await hubspotPost(token, '/crm/v3/objects/contacts', contact); | ||
| console.log(`[CRM Sync] Created HubSpot contact ${created.id} for customer ${customer.id}`); | ||
| } | ||
| }, | ||
|
|
||
| /** | ||
| * Logs a HubSpot note when a Chargebee subscription is created. | ||
| * @param {import('../types/types.d.ts').HandlerPayload} payload | ||
| */ | ||
| onSubscriptionCreated: async function (payload) { | ||
| const token = getToken(payload, 'hubspot'); | ||
| const subscription = payload.event.content.subscription; | ||
| const customer = payload.event.content.customer; | ||
|
|
||
| const note = { | ||
| properties: { | ||
| hs_note_body: [ | ||
| `Chargebee subscription created`, | ||
| `Subscription ID: ${subscription.id}`, | ||
| `Plan: ${subscription.subscription_items?.[0]?.item_price_id ?? 'N/A'}`, | ||
| `Status: ${subscription.status}`, | ||
| `Customer: ${customer?.email ?? subscription.customer_id}`, | ||
| ].join('\n'), | ||
| hs_timestamp: new Date().toISOString(), | ||
| }, | ||
| }; | ||
|
|
||
| const created = await hubspotPost(token, '/crm/v3/objects/notes', note); | ||
| console.log(`[CRM Sync] Created HubSpot note ${created.id} for subscription ${subscription.id}`); | ||
| }, | ||
| }; | ||
|
|
||
| // ── Helpers ────────────────────────────────────────────────────────────────── | ||
|
|
||
| /** | ||
| * Returns the Bearer token for a connector. | ||
| * Throws a descriptive error when the connector hasn't been authorized yet, | ||
| * so the developer sees a clear message in the tester UI logs. | ||
| */ | ||
| function getToken(payload, connectorName) { | ||
| const token = payload.oauth_token?.[connectorName]?.access_token; | ||
| if (!token) { | ||
| throw new Error( | ||
| `OAuth token for '${connectorName}' is not available. ` + | ||
| `Open the tester UI, go to the OAuth tab, and click Connect to authorize.` | ||
| ); | ||
| } | ||
| return token; | ||
| } | ||
|
|
||
| async function hubspotGet(token, path) { | ||
| const res = await fetch(`${HUBSPOT_API}${path}`, { | ||
| headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, | ||
| }); | ||
| if (!res.ok && res.status !== 404) { | ||
| throw new Error(`HubSpot GET ${path} failed: ${res.status} ${await res.text()}`); | ||
| } | ||
| return res.status === 404 ? {} : res.json(); | ||
| } | ||
|
|
||
| async function hubspotPost(token, path, body) { | ||
| const res = await fetch(`${HUBSPOT_API}${path}`, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| if (!res.ok) { | ||
| throw new Error(`HubSpot POST ${path} failed: ${res.status} ${await res.text()}`); | ||
| } | ||
| return res.json(); | ||
| } | ||
|
|
||
| async function hubspotPatch(token, path, body) { | ||
| const res = await fetch(`${HUBSPOT_API}${path}`, { | ||
| method: 'PATCH', | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| if (!res.ok) { | ||
| throw new Error(`HubSpot PATCH ${path} failed: ${res.status} ${await res.text()}`); | ||
| } | ||
| return res.json(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "name": "oauth-crm-sync", | ||
| "version": "1.0.0", | ||
| "description": "Sample app: syncs Chargebee customer and subscription events to a CRM via OAuth 2.0.", | ||
| "events": { | ||
| "customer_created": { | ||
| "handler": "onCustomerCreated" | ||
| }, | ||
| "subscription_created": { | ||
| "handler": "onSubscriptionCreated" | ||
| } | ||
| }, | ||
| "dependencies": {} | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| { | ||
| "connectors": { | ||
| "hubspot": { | ||
| "client_id": "<YOUR_HUBSPOT_CLIENT_ID>", | ||
| "client_secret": "<YOUR_HUBSPOT_CLIENT_SECRET>", | ||
| "authorize_url": "https://app.hubspot.com/oauth/authorize", | ||
| "token_url": "https://api.hubapi.com/oauth/v1/token", | ||
| "options": { | ||
| "scope": "crm.objects.contacts.read crm.objects.contacts.write" | ||
| } | ||
| } | ||
| } | ||
| } |
23 changes: 23 additions & 0 deletions
23
sample-apps/oauth-crm-sync/test_data/customer_created.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "id": "evt_sample_cust_001", | ||
| "api_version": "v2", | ||
| "object": "event", | ||
| "occurred_at": 1700000000, | ||
| "source": "api", | ||
| "webhook_status": "success", | ||
| "webhooks": [], | ||
| "event_type": "customer_created", | ||
| "content": { | ||
| "customer": { | ||
| "id": "cust_sample_001", | ||
| "email": "alex.sample@example.com", | ||
| "first_name": "Alex", | ||
| "last_name": "Sample", | ||
| "phone": "+1-555-0100", | ||
| "company": "Example Corp", | ||
| "created_at": 1700000000, | ||
| "updated_at": 1700000000, | ||
| "object": "customer" | ||
| } | ||
| } | ||
| } |
36 changes: 36 additions & 0 deletions
36
sample-apps/oauth-crm-sync/test_data/subscription_created.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "id": "evt_sample_sub_001", | ||
| "api_version": "v2", | ||
| "object": "event", | ||
| "occurred_at": 1700000060, | ||
| "source": "api", | ||
| "webhook_status": "success", | ||
| "webhooks": [], | ||
| "event_type": "subscription_created", | ||
| "content": { | ||
| "subscription": { | ||
| "id": "sub_sample_001", | ||
| "customer_id": "cust_sample_001", | ||
| "status": "active", | ||
| "created_at": 1700000060, | ||
| "updated_at": 1700000060, | ||
| "subscription_items": [ | ||
| { | ||
| "item_price_id": "starter-monthly-usd", | ||
| "quantity": 1, | ||
| "unit_price": 4900, | ||
| "amount": 4900, | ||
| "object": "subscription_item" | ||
| } | ||
| ], | ||
| "object": "subscription" | ||
| }, | ||
| "customer": { | ||
| "id": "cust_sample_001", | ||
| "email": "alex.sample@example.com", | ||
| "first_name": "Alex", | ||
| "last_name": "Sample", | ||
| "object": "customer" | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| interface OAuthAccessToken { | ||
| access_token: string; | ||
| token_type: string; | ||
| } | ||
|
|
||
| interface HandlerPayload { | ||
| event: import('@chargebee/chargebee-apps-shared').EventRecord; | ||
| iparams?: Record<string, any>; | ||
| oauth_token?: Record<string, OAuthAccessToken>; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cb-jananivijayan this looks like a manually created types.d.ts, for consistency can we have the same types.d.ts as the oauth template as created by the
chargebee-apps createcommand.