├── .gitignore ├── README.md ├── node ├── .env.template ├── .eslintignore ├── .eslintrc.js ├── README.md ├── package-lock.json ├── package.json ├── prettier.config.js └── src │ ├── config.js │ ├── index.js │ ├── public │ ├── css │ │ └── main.css │ ├── favicon-32x32.webp │ └── js │ │ └── login.js │ └── views │ ├── contacts.pug │ ├── error.pug │ ├── includes │ ├── footer.pug │ ├── head.pug │ ├── header.pug │ └── layout.pug │ └── login.pug ├── php ├── .env.template ├── .php-cs-fixer.php ├── README.md ├── composer.json ├── composer.lock └── src │ ├── Helpers │ ├── HubspotClientHelper.php │ ├── OAuth2Helper.php │ └── UrlHelper.php │ ├── actions │ ├── contacts │ │ └── list.php │ └── oauth │ │ ├── authorize.php │ │ ├── callback.php │ │ └── login.php │ ├── public │ ├── .htaccess │ ├── css │ │ └── main.css │ ├── index.php │ └── js │ │ └── login.js │ ├── routes │ ├── protected.php │ └── public.php │ └── views │ ├── _partials │ ├── footer.php │ └── header.php │ ├── contacts │ └── list.php │ ├── error.php │ └── oauth │ └── login.php ├── python ├── .env.template ├── README.md ├── app │ ├── __init__.py │ ├── auth.py │ ├── helpers │ │ ├── hubspot.py │ │ └── oauth.py │ ├── routes │ │ ├── __init__.py │ │ ├── contacts.py │ │ └── oauth.py │ ├── static │ │ └── styles │ │ │ └── main.css │ └── templates │ │ ├── contacts │ │ └── list.html │ │ ├── layout.html │ │ └── oauth │ │ └── login.html ├── requirements.txt └── run.py └── ruby ├── .env.template ├── .env.test.template ├── .gitignore ├── .rspec ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── app ├── assets │ ├── config │ │ └── manifest.js │ ├── images │ │ └── .keep │ ├── javascripts │ │ ├── application.js │ │ ├── cable.js │ │ └── channels │ │ │ └── .keep │ └── stylesheets │ │ └── application.css ├── controllers │ ├── application_controller.rb │ ├── concerns │ │ ├── .keep │ │ └── exception_handler.rb │ ├── contacts_controller.rb │ └── oauth │ │ └── authorization_controller.rb ├── helpers │ └── application_helper.rb ├── lib │ └── services │ │ ├── authorization │ │ ├── authorize_hubspot.rb │ │ ├── get_authorization_uri.rb │ │ └── tokens │ │ │ ├── base.rb │ │ │ ├── generate.rb │ │ │ └── refresh.rb │ │ └── hubspot │ │ ├── contacts │ │ ├── create.rb │ │ ├── destroy.rb │ │ ├── export.rb │ │ ├── get_by_id.rb │ │ ├── get_page.rb │ │ ├── search.rb │ │ └── update.rb │ │ └── owners │ │ └── get_all.rb ├── models │ ├── application_record.rb │ └── concerns │ │ └── .keep └── views │ ├── contacts │ ├── index.html.erb │ ├── new.html.erb │ └── show.html.erb │ ├── layouts │ └── application.html.erb │ ├── oauth │ └── authorization │ │ └── login.html.erb │ └── shared │ └── _header.html.erb ├── bin ├── bundle ├── rails ├── rake ├── setup ├── spring ├── update └── yarn ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── assets.rb │ ├── filter_parameter_logging.rb │ ├── mime_types.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb └── spring.rb ├── db └── seeds.rb ├── lib ├── assets │ └── .keep └── tasks │ └── .keep ├── log └── .keep ├── package.json ├── public ├── 404.html ├── 422.html ├── 500.html ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── favicon.ico └── robots.txt ├── spec ├── features │ ├── visitor_creates_new_contact_spec.rb │ ├── visitor_deletes_contact_spec.rb │ ├── visitor_downloads_contacts_spec.rb │ ├── visitor_edits_contact_spec.rb │ ├── visitor_lists_contacts_spec.rb │ ├── visitor_logs_in_spec.rb │ └── visitor_searches_contacts_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support │ ├── application_helper.rb │ └── download_helper.rb └── tmp └── .keep /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env-test 3 | 4 | # Node 5 | node_modules/ 6 | 7 | # Python 8 | *.egg-info 9 | __pycache__ 10 | 11 | #Php 12 | vendor/ 13 | .php-cs-fixer.cache 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HubSpot oauth sample app 2 | 3 | This is a sample app for the HubSpot [client libraries](https://developers.hubspot.com/docs/api/overview). This sample app demonstrates how to authenticate your HubSpot app using OAuth 2.0, and make an API call to the Contacts API. 4 | 5 | ## Reference 6 | 7 | - [Working with OAuth](https://developers.hubspot.com/docs/api/working-with-oauth) 8 | - [Contacts API ](https://developers.hubspot.com/docs/api/crm/contacts) 9 | 10 | ## How to run locally 11 | 12 | 1. The first steps is to [create a HubSpot developer account](https://developers.hubspot.com/docs/api/developer-tools-overview). This is where you will create and manage HubSpot apps. 13 | 2. Next [create an app](https://developers.hubspot.com/docs/api/creating-an-app). On the "App info" tab, You will be prompted to fill out some basic information about your app. This includes name, description, logo, etc. 14 | 15 | 3. Lastly, in the "Auth" tab, you will be provided with a Client ID & Client secret (used in the next step). To ensure your app is compatible with this sample app, set the "Redirect URL" ("http://localhost:3000/oauth-callback" for Node), and select "Contacts" from the "Scopes" list. 16 | 17 | 4. Copy the .env.template file into a file named .env in the folder of the language you want to use. For example: 18 | 19 | ```bash 20 | cp node/.env.template node/.env 21 | ``` 22 | 23 | 5. Paste your HubSpot Client Id and HubSpot Client Secret as the value for HUBSPOT_CLIENT_ID and HUBSPOT_CLIENT_SECRET in .env 24 | 25 | 6. Follow the language instructions on how to run. For example, if you want to run the Node server: 26 | 27 | ``` 28 | cd node # there's a README in this folder with instructions 29 | npm install 30 | npm run dev 31 | ``` 32 | 33 | ## Supported languages 34 | 35 | * [JavaScript (Node)](node/README.md) 36 | * [Python](python/README.md) 37 | * [Php](php/README.md) 38 | * [Ruby](ruby/README.md) 39 | 40 | ## Note on application scopes 41 | HubSpot provides a way to restrict application users access to the system to certain scopes. In order to do that it is a good practice to make a set of scopes required by your application. 42 | Please refer to [Initiate an Integration with OAuth 2.0](https://developers.hubspot.com/docs/methods/oauth2/initiate-oauth-integration) for documentation on the scope parameter passed to https://app.hubspot.com/oauth/authorize to make a set of scopes required. [Scopes](https://developers.hubspot.com/docs/methods/oauth2/initiate-oauth-integration#scopes) explains how to make optional scopes and talks about scopes available in HubSpot system 43 | 44 | -------------------------------------------------------------------------------- /node/.env.template: -------------------------------------------------------------------------------- 1 | HUBSPOT_CLIENT_ID= 2 | HUBSPOT_CLIENT_SECRET= 3 | -------------------------------------------------------------------------------- /node/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /node/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint:recommended', 3 | root: true, 4 | env: { 5 | browser: false, 6 | node: true, 7 | commonjs: true, 8 | es6: true, 9 | }, 10 | parserOptions: { 11 | ecmaVersion: 2018, 12 | }, 13 | rules: { 14 | 'no-console': 'off', 15 | 'no-return-await': 'error', 16 | }, 17 | 18 | overrides: [ 19 | { 20 | files: ['**/src/public/js/*.js'], 21 | env: { 22 | browser: true, 23 | node: false, 24 | }, 25 | }, 26 | ], 27 | }; 28 | -------------------------------------------------------------------------------- /node/README.md: -------------------------------------------------------------------------------- 1 | # HubSpot-nodejs oauth sample app 2 | 3 | An [Express server](http://expressjs.com/) implementation 4 | 5 | ## Requirements 6 | 7 | 1. Node v10+ 8 | 2. Redirect url [configured](https://github.com/HubSpot/sample-apps-oauth/blob/main/README.md#how-to-run-locally) to http://localhost:3000/oauth-callback 9 | 3. [Configured](https://github.com/HubSpot/sample-apps-oauth/blob/main/README.md#how-to-run-locally) .env file 10 | 11 | ## Running 12 | 13 | 1. Install dependencies 14 | 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | 1. Run the application 20 | 21 | ```bash 22 | npm run dev 23 | ``` 24 | 25 | You should now be able to navigate to [http://localhost:3000](http://localhost:3000) and use the application. 26 | 27 | ## Configure OAuth 28 | 29 | Copy redirect url from login page and designate this on your app's Auth settings page. 30 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubspot-oauth", 3 | "version": "2.0.0", 4 | "engines": { 5 | "node": ">= 10.13.0" 6 | }, 7 | "description": "hubspot-node client sample applications", 8 | "main": "src/index.js", 9 | "scripts": { 10 | "start": "src/index.js", 11 | "dev": "nodemon src/index.js", 12 | "lint": "eslint . && prettier --list-different './**/*.{js,json}'", 13 | "prettier:write": "prettier --write './**/*.{js,json}'" 14 | }, 15 | "keywords": [ 16 | "hubspot", 17 | "oauth", 18 | "contacts", 19 | "sample", 20 | "example" 21 | ], 22 | "author": "hubspot", 23 | "license": "Apache-2.0", 24 | "dependencies": { 25 | "@hubspot/api-client": "^11.0.0-beta.0", 26 | "body-parser": "^1.20.2", 27 | "dotenv": "^16.3.1", 28 | "express": "^4.18.2", 29 | "lodash": "^4.17.21", 30 | "pug": "^3.0.2" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^8.47.0", 34 | "nodemon": "^3.0.1", 35 | "prettier": "^3.0.2" 36 | }, 37 | "repository": { 38 | "type": "git", 39 | "url": "https://github.com/HubSpot/sample-apps-oauth.git" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /node/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bracketSpacing: true, 3 | bracketSameLine: false, 4 | printWidth: 80, 5 | proseWrap: 'never', 6 | semi: true, 7 | singleQuote: true, 8 | tabWidth: 2, 9 | // TODO: Change to `all` once supported 10 | trailingComma: 'es5', 11 | useTabs: false, 12 | overrides: [ 13 | { 14 | files: ['static_conf.json'], 15 | options: { 16 | parser: 'json-stringify', 17 | }, 18 | }, 19 | { 20 | files: ['*.lyaml'], 21 | options: { 22 | // Prevent unparsable wrapping behavior in lyaml files: 23 | // https://git.hubteam.com/HubSpot/prettier-config-hubspot/pull/36 24 | printWidth: 999, 25 | // Wrapping looks bad for certain strings that use newlines to separate 26 | // JSX elements 27 | proseWrap: 'preserve', 28 | }, 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /node/src/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ path: '.env' }); 2 | -------------------------------------------------------------------------------- /node/src/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | const hubspot = require('@hubspot/api-client'); 5 | const bodyParser = require('body-parser'); 6 | require('./config'); 7 | 8 | const PORT = 3000; 9 | const OBJECTS_LIMIT = 30; 10 | const CLIENT_ID = process.env.HUBSPOT_CLIENT_ID; 11 | const CLIENT_SECRET = process.env.HUBSPOT_CLIENT_SECRET; 12 | const SCOPES = 'crm.objects.contacts.read'; 13 | const REDIRECT_URI = `http://localhost:${PORT}/oauth-callback`; 14 | const GRANT_TYPES = { 15 | AUTHORIZATION_CODE: 'authorization_code', 16 | REFRESH_TOKEN: 'refresh_token', 17 | }; 18 | 19 | let tokenStore = {}; 20 | 21 | const logResponse = (message, data) => { 22 | console.log(message, JSON.stringify(data, null, 1)); 23 | }; 24 | 25 | const checkEnv = (req, res, next) => { 26 | if (_.startsWith(req.url, '/error')) return next(); 27 | 28 | if (_.isNil(CLIENT_ID)) 29 | return res.redirect( 30 | '/error?msg=Please set HUBSPOT_CLIENT_ID env variable to proceed' 31 | ); 32 | if (_.isNil(CLIENT_SECRET)) 33 | return res.redirect( 34 | '/error?msg=Please set HUBSPOT_CLIENT_SECRET env variable to proceed' 35 | ); 36 | 37 | next(); 38 | }; 39 | 40 | const isAuthorized = () => { 41 | return !_.isEmpty(tokenStore.refreshToken); 42 | }; 43 | 44 | const isTokenExpired = () => { 45 | return Date.now() >= tokenStore.updatedAt + tokenStore.expiresIn * 1000; 46 | }; 47 | 48 | const prepareContactsContent = (contacts) => { 49 | return _.map(contacts, (contact) => { 50 | const companyName = _.get(contact, 'properties.company') || ''; 51 | const name = getFullName(contact.properties); 52 | return { id: contact.id, name, companyName }; 53 | }); 54 | }; 55 | 56 | const getFullName = (contactProperties) => { 57 | const firstName = _.get(contactProperties, 'firstname') || ''; 58 | const lastName = _.get(contactProperties, 'lastname') || ''; 59 | return `${firstName} ${lastName}`; 60 | }; 61 | 62 | const refreshToken = async () => { 63 | const result = await hubspotClient.oauth.tokensApi.create( 64 | GRANT_TYPES.REFRESH_TOKEN, 65 | undefined, 66 | undefined, 67 | CLIENT_ID, 68 | CLIENT_SECRET, 69 | tokenStore.refreshToken 70 | ); 71 | tokenStore = result; 72 | tokenStore.updatedAt = Date.now(); 73 | console.log('Updated tokens', tokenStore); 74 | 75 | hubspotClient.setAccessToken(tokenStore.accessToken); 76 | }; 77 | 78 | const handleError = (e, res) => { 79 | if (_.isEqual(e.message, 'HTTP request failed')) { 80 | const errorMessage = JSON.stringify(e, null, 2); 81 | console.error(errorMessage); 82 | return res.redirect(`/error?msg=${errorMessage}`); 83 | } 84 | 85 | console.error(e); 86 | res.redirect( 87 | `/error?msg=${JSON.stringify(e, Object.getOwnPropertyNames(e), 2)}` 88 | ); 89 | }; 90 | 91 | const app = express(); 92 | 93 | const hubspotClient = new hubspot.Client(); 94 | 95 | app.use(express.static(path.join(__dirname, 'public'))); 96 | app.set('view engine', 'pug'); 97 | app.set('views', path.join(__dirname, 'views')); 98 | 99 | app.use( 100 | bodyParser.urlencoded({ 101 | limit: '50mb', 102 | extended: true, 103 | }) 104 | ); 105 | 106 | app.use( 107 | bodyParser.json({ 108 | limit: '50mb', 109 | extended: true, 110 | }) 111 | ); 112 | 113 | app.use(checkEnv); 114 | 115 | app.get('/', async (req, res) => { 116 | try { 117 | if (!isAuthorized()) return res.redirect('/login'); 118 | if (isTokenExpired()) await refreshToken(); 119 | 120 | const properties = ['firstname', 'lastname', 'company']; 121 | 122 | // Get first contacts page 123 | // GET /crm/v3/objects/contacts 124 | // https://developers.hubspot.com/docs/api/crm/contacts 125 | console.log('Calling crm.contacts.basicApi.getPage. Retrieve contacts.'); 126 | const contactsResponse = await hubspotClient.crm.contacts.basicApi.getPage( 127 | OBJECTS_LIMIT, 128 | undefined, 129 | properties 130 | ); 131 | logResponse('Response from API', contactsResponse); 132 | 133 | res.render('contacts', { 134 | tokenStore, 135 | contacts: prepareContactsContent(contactsResponse.results), 136 | }); 137 | } catch (e) { 138 | handleError(e, res); 139 | } 140 | }); 141 | 142 | app.use('/oauth', async (req, res) => { 143 | // Use the client to get authorization Url 144 | // https://www.npmjs.com/package/@hubspot/api-client#obtain-your-authorization-url 145 | console.log('Creating authorization Url'); 146 | const authorizationUrl = hubspotClient.oauth.getAuthorizationUrl( 147 | CLIENT_ID, 148 | REDIRECT_URI, 149 | SCOPES 150 | ); 151 | console.log('Authorization Url', authorizationUrl); 152 | 153 | res.redirect(authorizationUrl); 154 | }); 155 | 156 | app.use('/oauth-callback', async (req, res) => { 157 | const code = _.get(req, 'query.code'); 158 | 159 | // Create OAuth 2.0 Access Token and Refresh Tokens 160 | // POST /oauth/v1/token 161 | // https://developers.hubspot.com/docs/api/working-with-oauth 162 | console.log('Retrieving access token by code:', code); 163 | const getTokensResponse = await hubspotClient.oauth.tokensApi.create( 164 | GRANT_TYPES.AUTHORIZATION_CODE, 165 | code, 166 | REDIRECT_URI, 167 | CLIENT_ID, 168 | CLIENT_SECRET 169 | ); 170 | logResponse('Retrieving access token result:', getTokensResponse); 171 | 172 | tokenStore = getTokensResponse; 173 | tokenStore.updatedAt = Date.now(); 174 | 175 | // Set token for the 176 | // https://www.npmjs.com/package/@hubspot/api-client 177 | hubspotClient.setAccessToken(tokenStore.accessToken); 178 | res.redirect('/'); 179 | }); 180 | 181 | app.get('/login', (req, res) => { 182 | tokenStore = {}; 183 | res.render('login', { redirectUri: REDIRECT_URI }); 184 | }); 185 | 186 | app.get('/refresh', async (req, res) => { 187 | try { 188 | if (isAuthorized()) await refreshToken(); 189 | res.redirect('/'); 190 | } catch (e) { 191 | handleError(e, res); 192 | } 193 | }); 194 | 195 | app.get('/error', (req, res) => { 196 | res.render('error', { error: req.query.msg }); 197 | }); 198 | 199 | app.use((error, req, res) => { 200 | res.render('error', { error: error.message }); 201 | }); 202 | 203 | app.listen(PORT, () => console.log(`Listening on http://localhost:${PORT}`)); 204 | -------------------------------------------------------------------------------- /node/src/public/css/main.css: -------------------------------------------------------------------------------- 1 | .wrapper .container { 2 | padding-bottom: 2rem; 3 | padding-top: 2rem; 4 | } 5 | 6 | .navigation { 7 | background: #f4f5f6; 8 | border-bottom: .1rem solid #d1d1d1; 9 | display: block; 10 | height: 5.2rem; 11 | left: 0; 12 | max-width: 100%; 13 | width: 100%; 14 | } 15 | 16 | .navigation .container { 17 | padding-bottom: 0; 18 | padding-top: 0 19 | } 20 | 21 | .navigation .navigation-list { 22 | list-style: none; 23 | margin-bottom: 0; 24 | } 25 | 26 | .navigation .navigation-item { 27 | float: left; 28 | margin-bottom: 0; 29 | margin-left: 2.5rem; 30 | position: relative 31 | } 32 | 33 | .navigation .navigation-title, .navigation .title { 34 | color: #606c76; 35 | position: relative 36 | } 37 | 38 | .navigation .navigation-link, .navigation .navigation-title, .navigation .title { 39 | display: inline; 40 | font-size: 1.6rem; 41 | line-height: 5.2rem; 42 | padding: 0; 43 | text-decoration: none 44 | } 45 | 46 | .navigation .navigation-link.active { 47 | color: #606c76 48 | } 49 | 50 | .text-center { 51 | text-align: center; 52 | } 53 | 54 | .tokens-table { 55 | max-width: 600px; 56 | } 57 | 58 | .tokens-table td { 59 | white-space: nowrap; 60 | overflow: hidden; 61 | text-overflow: ellipsis; 62 | font-size: small; 63 | } 64 | -------------------------------------------------------------------------------- /node/src/public/favicon-32x32.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/node/src/public/favicon-32x32.webp -------------------------------------------------------------------------------- /node/src/public/js/login.js: -------------------------------------------------------------------------------- 1 | document.getElementById('copyBtn').onclick = async () => { 2 | let text = document.getElementById('redirectURL').textContent; 3 | await navigator.clipboard.writeText(text); 4 | 5 | alert('Copied'); 6 | }; 7 | -------------------------------------------------------------------------------- /node/src/views/contacts.pug: -------------------------------------------------------------------------------- 1 | extends includes/layout 2 | block content 3 | - const getTokenApiDetails = ' Get OAuth 2.0 Access Token and Refresh Tokens\n POST /oauth/v1/token\n oauth.defaultApi.createToken' 4 | - const getContactsApiDetails = ' Get all companies\n GET /crm/v3/objects/contacts\n crm.contacts.basicApi.getPage' 5 | .container 6 | h3 Tokens 7 | pre #{getTokenApiDetails} 8 | table(class='tokens-table') 9 | thead 10 | tr 11 | th Name 12 | th Value 13 | tbody 14 | tr 15 | td Access Token 16 | td #{tokenStore.accessToken || ''} 17 | tr 18 | td Refresh Token 19 | td #{tokenStore.refreshToken || ''} 20 | tr 21 | td Expires In 22 | td #{tokenStore.expiresIn || ''} 23 | tr 24 | td Updated At 25 | td #{tokenStore.updatedAt ? new Date(tokenStore.updatedAt) : ''} 26 | 27 | h3 Contacts 28 | pre #{getContactsApiDetails} 29 | table 30 | thead 31 | tr 32 | th ID 33 | th Name 34 | th Company 35 | tbody 36 | each contact in contacts 37 | tr 38 | td #{contact.id} 39 | td #{contact.name} 40 | td #{contact.companyName} 41 | -------------------------------------------------------------------------------- /node/src/views/error.pug: -------------------------------------------------------------------------------- 1 | extends includes/layout 2 | block content 3 | .container 4 | h4 Error 5 | pre #{error} 6 | -------------------------------------------------------------------------------- /node/src/views/includes/footer.pug: -------------------------------------------------------------------------------- 1 | block footerScripts 2 | .footer 3 | .container 4 | -------------------------------------------------------------------------------- /node/src/views/includes/head.pug: -------------------------------------------------------------------------------- 1 | head 2 | meta(charset='UTF-8') 3 | meta(name='description' content='HubSpot JavaScript Sample Oauth') 4 | title HubSpot JavaScript Sample Oauth 5 | link(rel='stylesheet' href='//fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic') 6 | link(rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css') 7 | link(rel='stylesheet' href='//cdnjs.cloudflare.com/ajax/libs/milligram/1.3.0/milligram.css') 8 | link(rel='stylesheet' href='/css/main.css') 9 | // Fav Icon 10 | link(href='/favicon-32x32.webp' rel='shortcut icon') 11 | link(href='/favicon-32x32.webp' rel='apple-touch-icon') 12 | 13 | script(type='application/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js') 14 | -------------------------------------------------------------------------------- /node/src/views/includes/header.pug: -------------------------------------------------------------------------------- 1 | header 2 | .navigation 3 | .container 4 | a(class='navigation-title' href='/') 5 | h3(class='title') HubSpot JavaScript Sample OAuth2 6 | 7 | ul(class='navigation-list float-right') 8 | li(class='navigation-item') 9 | a(class='navigation-link' href='/refresh') Refresh Access Token 10 | li(class='navigation-item') 11 | a(class='navigation-link' href='/login') OAuth2 12 | -------------------------------------------------------------------------------- /node/src/views/includes/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='en') 3 | include head 4 | body 5 | main(class='wrapper') 6 | include header 7 | block content 8 | include footer 9 | -------------------------------------------------------------------------------- /node/src/views/login.pug: -------------------------------------------------------------------------------- 1 | extends includes/layout 2 | block content 3 | .container.text-center 4 | h3 In order to continue please update the redirect URL on Auth settings page of your app 5 | h4 Redirect URL 6 | pre#redirectURL #{redirectUri} 7 | button.button-primary#copyBtn Copy 8 | h3 After that authorize via OAuth 9 | .authorize-button 10 | a(class='button' href='/oauth') Authorize 11 | block footerScripts 12 | script(type='application/javascript' src='/js/login.js') 13 | -------------------------------------------------------------------------------- /php/.env.template: -------------------------------------------------------------------------------- 1 | HUBSPOT_CLIENT_ID= 2 | HUBSPOT_CLIENT_SECRET= 3 | -------------------------------------------------------------------------------- /php/.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([__DIR__]) 5 | ->exclude(['vendor']) 6 | ->notPath('/cache/') 7 | ; 8 | 9 | $config = new PhpCsFixer\Config(); 10 | 11 | return $config 12 | ->setFinder($finder) 13 | ->setRules([ 14 | '@PSR2' => true, 15 | '@PhpCsFixer' => true, 16 | ]) 17 | ; -------------------------------------------------------------------------------- /php/README.md: -------------------------------------------------------------------------------- 1 | # HubSpot-Php sample OAuth 2.0 app 2 | 3 | ## Setup App 4 | 5 | Make sure you have [Php 7.4+](https://www.php.net/downloads) and [Composer](https://getcomposer.org) installed. 6 | 7 | ## Configure 8 | 9 | 1. Copy .env.template to .env 10 | 2. Paste your HUBSPOT_CLIENT_ID and HUBSPOT_CLIENT_SECRET 11 | 12 | ## Install 13 | 14 | ```bash 15 | composer i 16 | ``` 17 | 18 | ## Running 19 | 20 | ```bash 21 | php -S localhost:8000 -t src/public 22 | ``` 23 | 24 | You should now be able to navigate to [http://localhost:8000](http://localhost:8000) and use the application. 25 | 26 | ## Configure OAuth 27 | 28 | Copy redirect url from login page and designate this on your app's Auth settings page. 29 | -------------------------------------------------------------------------------- /php/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "minimum-stability": "stable", 3 | "require": { 4 | "hubspot/api-client": "^12.0.0", 5 | "ext-curl": "*", 6 | "ext-json": "*", 7 | "vlucas/phpdotenv": "^5.5" 8 | }, 9 | "require-dev": { 10 | "friendsofphp/php-cs-fixer": "^3.24.0" 11 | }, 12 | "autoload": { 13 | "psr-4": { 14 | "": "src/" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /php/src/Helpers/HubspotClientHelper.php: -------------------------------------------------------------------------------- 1 | $tokens->getAccessToken(), 48 | 'refresh_token' => $tokens->getRefreshToken(), 49 | 'expires_in' => $tokens->getExpiresIn(), 50 | 'expires_at' => time() + $tokens['expires_in'] * 0.95, 51 | ]; 52 | } 53 | 54 | public static function isAuthenticated(): bool 55 | { 56 | return isset($_SESSION[static::SESSION_TOKENS_KEY]); 57 | } 58 | 59 | public static function refreshAndGetAccessToken(): string 60 | { 61 | if (empty($_SESSION[static::SESSION_TOKENS_KEY])) { 62 | throw new \Exception('Please authorize via OAuth2'); 63 | } 64 | 65 | $tokens = $_SESSION[static::SESSION_TOKENS_KEY]; 66 | 67 | if (time() > $tokens['expires_at']) { 68 | $tokens = Factory::create()->auth()->oAuth()->tokensApi()->create( 69 | 'refresh_token', 70 | null, 71 | static::getRedirectUri(), 72 | static::getClientId(), 73 | static::getClientSecret(), 74 | $tokens['refresh_token'] 75 | ); 76 | self::saveTokenResponse($tokens); 77 | } 78 | 79 | return $tokens['access_token']; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /php/src/Helpers/UrlHelper.php: -------------------------------------------------------------------------------- 1 | crm()->contacts()->basicApi()->getPage(); 11 | 12 | include __DIR__.'/../../views/contacts/list.php'; 13 | -------------------------------------------------------------------------------- /php/src/actions/oauth/authorize.php: -------------------------------------------------------------------------------- 1 | oauth()->tokensApi()->create( 8 | 'authorization_code', 9 | $_GET['code'], 10 | OAuth2Helper::getRedirectUri(), 11 | OAuth2Helper::getClientId(), 12 | OAuth2Helper::getClientSecret() 13 | ); 14 | 15 | OAuth2Helper::saveTokenResponse($tokens); 16 | 17 | header('Location: /'); 18 | -------------------------------------------------------------------------------- /php/src/actions/oauth/login.php: -------------------------------------------------------------------------------- 1 | load(); 12 | 13 | $publicRoutes = require '../routes/public.php'; 14 | $protectedRoutes = require '../routes/protected.php'; 15 | 16 | if (in_array($uri, $protectedRoutes)) { 17 | if (!OAuth2Helper::isAuthenticated()) { 18 | header('Location: /oauth/login'); 19 | } 20 | } 21 | 22 | if ('/' === $uri) { 23 | header('Location: /contacts/list'); 24 | 25 | exit; 26 | } 27 | 28 | if (!in_array($uri, array_merge($publicRoutes, $protectedRoutes))) { 29 | http_response_code(404); 30 | 31 | exit; 32 | } 33 | 34 | $path = __DIR__.'/../actions'.$uri.'.php'; 35 | 36 | require $path; 37 | } catch (Throwable $t) { 38 | $message = $t->getMessage(); 39 | 40 | include __DIR__.'/../views/error.php'; 41 | 42 | exit; 43 | } 44 | -------------------------------------------------------------------------------- /php/src/public/js/login.js: -------------------------------------------------------------------------------- 1 | document.getElementById('copyBtn').onclick = async () => { 2 | let text = document.getElementById('redirectURL').textContent; 3 | await navigator.clipboard.writeText(text); 4 | 5 | alert('Copied'); 6 | }; 7 | -------------------------------------------------------------------------------- /php/src/routes/protected.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /php/src/views/_partials/header.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HubSpot PHP Sample OAuth 2.0 App 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 28 |
29 | -------------------------------------------------------------------------------- /php/src/views/contacts/list.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
 4 | // src/actions/contacts/list.php
 5 | $hubSpot->crm()->contacts()->basicApi()->getPage();
 6 | 
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | getResults() as $contact) { ?> 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
IDEmailName
getId(); ?>getProperties()['email']); ?>getProperties()['firstname'].' '.$contact->getProperties()['lastname']); ?>
27 | 28 | 29 | -------------------------------------------------------------------------------- /php/src/views/error.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | 6 | -------------------------------------------------------------------------------- /php/src/views/oauth/login.php: -------------------------------------------------------------------------------- 1 | 6 |
7 |

In order to continue please update the redirect URL on Auth settings page of your app

8 |

Redirect URL

9 |
10 | 11 |

After that authorize via OAuth

12 |
13 | Authorize 14 |
15 |
16 |
17 | // src/actions/oauth/authorize.php - Generate URL for OAuth
18 | $authUrl = HubSpot\Utils\OAuth2::getAuthUrl(
19 |     'ClientID',
20 |     'Redirect Uri',
21 |     ['Scopes']
22 | );
23 | 
24 | 25 | 26 | -------------------------------------------------------------------------------- /python/.env.template: -------------------------------------------------------------------------------- 1 | HUBSPOT_CLIENT_ID= 2 | HUBSPOT_CLIENT_SECRET= 3 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | # HubSpot-python sample OAuth 2.0 app 2 | 3 | ### Setup App 4 | 5 | Make sure you have [Python 3.8+](https://www.python.org/downloads/) installed.\ 6 | Before using this app you should read [Documentation](https://developers.hubspot.com/docs/api/working-with-oauth) 7 | 8 | ### Configure 9 | 10 | 1. Copy .env.template to .env 11 | 2. Paste your HUBSPOT_CLIENT_ID and HUBSPOT_CLIENT_SECRET 12 | 13 | ### Install 14 | 15 | ```bash 16 | pip3 install -r requirements.txt 17 | ``` 18 | 19 | ### Running 20 | 21 | ```bash 22 | python3 run.py 23 | ``` 24 | 25 | You should now be able to navigate to [http://localhost:5000](http://localhost:5000) and use the application. 26 | -------------------------------------------------------------------------------- /python/app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, redirect, url_for 2 | 3 | import app.routes as routes 4 | 5 | app = Flask(__name__, template_folder="templates", static_folder="static") 6 | 7 | app.secret_key = b'_5#y2L"F4Q8z\n\xec]/' 8 | 9 | app.register_blueprint(routes.oauth, url_prefix="/oauth") 10 | app.register_blueprint(routes.contacts, url_prefix="/contacts") 11 | 12 | @app.route("/") 13 | def contacts(): 14 | return redirect(url_for("contacts.list")) 15 | -------------------------------------------------------------------------------- /python/app/auth.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, url_for 2 | from functools import wraps 3 | from .helpers.oauth import is_authorized 4 | 5 | 6 | def auth_required(func): 7 | @wraps(func) 8 | def check_auth(*args, **kwargs): 9 | if not is_authorized(): 10 | return redirect(url_for("oauth.login")) 11 | 12 | return func(*args, **kwargs) 13 | 14 | return check_auth 15 | -------------------------------------------------------------------------------- /python/app/helpers/hubspot.py: -------------------------------------------------------------------------------- 1 | from hubspot import HubSpot 2 | from .oauth import refresh_and_get_access_token, is_authorized 3 | 4 | 5 | def create_client(): 6 | if is_authorized(): 7 | return HubSpot(access_token=refresh_and_get_access_token()) 8 | 9 | return HubSpot() 10 | -------------------------------------------------------------------------------- /python/app/helpers/oauth.py: -------------------------------------------------------------------------------- 1 | import time 2 | import os 3 | from flask import session, request 4 | from hubspot import HubSpot 5 | 6 | TOKENS_KEY = "tokens" 7 | 8 | 9 | def save_tokens(tokens_response): 10 | tokens = { 11 | "access_token": tokens_response.access_token, 12 | "refresh_token": tokens_response.refresh_token, 13 | "expires_in": tokens_response.expires_in, 14 | "expires_at": time.time() + tokens_response.expires_in * 0.95, 15 | } 16 | session[TOKENS_KEY] = tokens 17 | 18 | return tokens 19 | 20 | 21 | def is_authorized(): 22 | return TOKENS_KEY in session 23 | 24 | 25 | def get_redirect_uri(): 26 | return request.host_url + "oauth/callback" 27 | 28 | 29 | def refresh_and_get_access_token(): 30 | if TOKENS_KEY not in session: 31 | raise Exception("No refresh token is specified") 32 | tokens = session[TOKENS_KEY] 33 | if time.time() > tokens["expires_at"]: 34 | tokens = HubSpot().auth.oauth.tokens_api.create( 35 | grant_type="refresh_token", 36 | redirect_uri=get_redirect_uri(), 37 | refresh_token=tokens["refresh_token"], 38 | client_id=os.environ.get("HUBSPOT_CLIENT_ID"), 39 | client_secret=os.environ.get("HUBSPOT_CLIENT_SECRET"), 40 | ) 41 | tokens = save_tokens(tokens) 42 | 43 | return tokens["access_token"] 44 | -------------------------------------------------------------------------------- /python/app/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .oauth import module as oauth 2 | from .contacts import module as contacts 3 | -------------------------------------------------------------------------------- /python/app/routes/contacts.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template 2 | from ..helpers.hubspot import create_client 3 | from ..auth import auth_required 4 | 5 | 6 | module = Blueprint("contacts", __name__) 7 | 8 | 9 | @module.route("/") 10 | @auth_required 11 | def list(): 12 | hubspot = create_client() 13 | contacts_page = hubspot.crm.contacts.basic_api.get_page() 14 | 15 | return render_template("contacts/list.html", contacts=contacts_page.results) 16 | -------------------------------------------------------------------------------- /python/app/routes/oauth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, redirect, request 2 | from hubspot.utils.oauth import get_auth_url 3 | import os 4 | from ..helpers.oauth import save_tokens, get_redirect_uri 5 | from ..helpers.hubspot import create_client 6 | 7 | module = Blueprint("oauth", __name__) 8 | 9 | 10 | @module.route("/login") 11 | def login(): 12 | return render_template("oauth/login.html") 13 | 14 | 15 | @module.route("/authorize") 16 | def authorize(): 17 | auth_url = get_auth_url( 18 | scope=("crm.objects.contacts.read",), 19 | client_id=os.environ.get("HUBSPOT_CLIENT_ID"), 20 | redirect_uri=get_redirect_uri(), 21 | ) 22 | 23 | return redirect(auth_url) 24 | 25 | 26 | @module.route("/callback") 27 | def callback(): 28 | hubspot = create_client() 29 | tokens_response = hubspot.auth.oauth.tokens_api.create( 30 | grant_type="authorization_code", 31 | code=request.args.get("code"), 32 | redirect_uri=get_redirect_uri(), 33 | client_id=os.environ.get("HUBSPOT_CLIENT_ID"), 34 | client_secret=os.environ.get("HUBSPOT_CLIENT_SECRET"), 35 | ) 36 | save_tokens(tokens_response) 37 | 38 | return redirect("/") 39 | -------------------------------------------------------------------------------- /python/app/static/styles/main.css: -------------------------------------------------------------------------------- 1 | .wrapper .container { 2 | padding-bottom: 2rem; 3 | padding-top: 2rem; 4 | } 5 | 6 | .navigation { 7 | background: #f4f5f6; 8 | border-bottom: .1rem solid #d1d1d1; 9 | display: block; 10 | height: 5.2rem; 11 | left: 0; 12 | max-width: 100%; 13 | width: 100%; 14 | } 15 | 16 | .navigation .container { 17 | padding-bottom: 0; 18 | padding-top: 0 19 | } 20 | 21 | .navigation .navigation-list { 22 | list-style: none; 23 | margin-bottom: 0; 24 | } 25 | 26 | .navigation .navigation-item { 27 | float: left; 28 | margin-bottom: 0; 29 | margin-left: 2.5rem; 30 | position: relative 31 | } 32 | 33 | .navigation .navigation-title, .navigation .title { 34 | color: #606c76; 35 | position: relative 36 | } 37 | 38 | .navigation .navigation-link, .navigation .navigation-title, .navigation .title { 39 | display: inline; 40 | font-size: 1.6rem; 41 | line-height: 5.2rem; 42 | padding: 0; 43 | text-decoration: none 44 | } 45 | 46 | .navigation .navigation-link.active { 47 | color: #606c76 48 | } 49 | 50 | pre { 51 | padding-left: 1rem; 52 | } 53 | 54 | .authorize-button { 55 | justify-content: center; 56 | } 57 | 58 | .text-center { 59 | text-align: center; 60 | } 61 | -------------------------------------------------------------------------------- /python/app/templates/contacts/list.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 | 5 |
 6 | // app/routes/contacts.py
 7 | client = HubSpot(access_token='access_token')
 8 | contacts_page = client.crm.contacts.basic_api.get_page()
 9 | 
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {% for contact in contacts %} 22 | 23 | 24 | 25 | 26 | 27 | {% endfor %} 28 | 29 | 30 |
IDEmailName
{{ contact.id }}{{ contact.properties.email }}{{ contact.properties.firstname ~ ' ' ~ contact.properties.lastname}}
31 | 32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /python/app/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HubSpot Python sample oauth app 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 28 |
29 | {% block content %} 30 | {% endblock %} 31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /python/app/templates/oauth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |
 5 | // app/routes/oauth.py - Generate URL for OAuth
 6 | from hubspot.utils.oauth import get_auth_url
 7 | auth_url = get_auth_url(
 8 |     scopes=('contacts',),
 9 |     client_id=os.environ.get('HUBSPOT_CLIENT_ID'),
10 |     redirect_uri=get_redirect_uri(),
11 | )
12 | 
13 | 14 |

In order to continue please authorize via OAuth

15 |
16 | Authorize 17 |
18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.3 2 | hubspot-api-client==9.0.0 3 | python-dotenv==1.0.1 -------------------------------------------------------------------------------- /python/run.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | 3 | if __name__ == "__main__": 4 | app.run(host="localhost", debug=True) 5 | -------------------------------------------------------------------------------- /ruby/.env.template: -------------------------------------------------------------------------------- 1 | HUBSPOT_CLIENT_ID= 2 | HUBSPOT_CLIENT_SECRET= 3 | -------------------------------------------------------------------------------- /ruby/.env.test.template: -------------------------------------------------------------------------------- 1 | HUBSPOT_EMAIL= 2 | HUBSPOT_PASSWORD= 3 | HUBSPOT_USER_ID= 4 | -------------------------------------------------------------------------------- /ruby/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile '~/.gitignore_global' 6 | 7 | # Ignore bundler config. 8 | /.bundle 9 | 10 | # Ignore the default SQLite database. 11 | /db/*.sqlite3 12 | /db/*.sqlite3-journal 13 | 14 | # Ignore all logfiles and tempfiles. 15 | /log/* 16 | /tmp/* 17 | !/log/.keep 18 | !/tmp/.keep 19 | 20 | # Ignore uploaded files in development 21 | /storage/* 22 | !/storage/.keep 23 | 24 | /node_modules 25 | /yarn-error.log 26 | 27 | /public/assets 28 | .byebug_history 29 | 30 | # Ignore master key for decrypting credentials and more. 31 | /config/master.key 32 | 33 | .rubocop.yml 34 | .env 35 | .env.test 36 | -------------------------------------------------------------------------------- /ruby/.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /ruby/.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-3.1.3 -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" } 3 | 4 | ruby '3.1.3' 5 | 6 | gem 'rails' 7 | gem 'sqlite3' 8 | gem 'puma', '~> 4.3.5' 9 | gem 'sass-rails', '~> 5.0' 10 | gem 'uglifier', '>= 1.3.0' 11 | gem 'jbuilder', '~> 2.5' 12 | gem 'csv' 13 | 14 | gem 'hubspot-api-client' 15 | 16 | group :development, :test do 17 | gem 'dotenv-rails', require: 'dotenv/rails-now' 18 | gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] 19 | gem 'rspec-rails', '~> 3.5' 20 | gem 'capybara' 21 | gem 'capybara-mechanize', '~> 1.11' 22 | gem 'selenium-webdriver', '3.4.3' 23 | gem 'geckodriver-helper' 24 | gem 'pry' 25 | gem 'pry-byebug' 26 | end 27 | 28 | group :development do 29 | gem 'web-console', '>= 3.3.0' 30 | gem 'listen', '>= 3.0.5', '< 3.2' 31 | end 32 | 33 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 34 | -------------------------------------------------------------------------------- /ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (7.0.4.3) 5 | actionpack (= 7.0.4.3) 6 | activesupport (= 7.0.4.3) 7 | nio4r (~> 2.0) 8 | websocket-driver (>= 0.6.1) 9 | actionmailbox (7.0.4.3) 10 | actionpack (= 7.0.4.3) 11 | activejob (= 7.0.4.3) 12 | activerecord (= 7.0.4.3) 13 | activestorage (= 7.0.4.3) 14 | activesupport (= 7.0.4.3) 15 | mail (>= 2.7.1) 16 | net-imap 17 | net-pop 18 | net-smtp 19 | actionmailer (7.0.4.3) 20 | actionpack (= 7.0.4.3) 21 | actionview (= 7.0.4.3) 22 | activejob (= 7.0.4.3) 23 | activesupport (= 7.0.4.3) 24 | mail (~> 2.5, >= 2.5.4) 25 | net-imap 26 | net-pop 27 | net-smtp 28 | rails-dom-testing (~> 2.0) 29 | actionpack (7.0.4.3) 30 | actionview (= 7.0.4.3) 31 | activesupport (= 7.0.4.3) 32 | rack (~> 2.0, >= 2.2.0) 33 | rack-test (>= 0.6.3) 34 | rails-dom-testing (~> 2.0) 35 | rails-html-sanitizer (~> 1.0, >= 1.2.0) 36 | actiontext (7.0.4.3) 37 | actionpack (= 7.0.4.3) 38 | activerecord (= 7.0.4.3) 39 | activestorage (= 7.0.4.3) 40 | activesupport (= 7.0.4.3) 41 | globalid (>= 0.6.0) 42 | nokogiri (>= 1.8.5) 43 | actionview (7.0.4.3) 44 | activesupport (= 7.0.4.3) 45 | builder (~> 3.1) 46 | erubi (~> 1.4) 47 | rails-dom-testing (~> 2.0) 48 | rails-html-sanitizer (~> 1.1, >= 1.2.0) 49 | activejob (7.0.4.3) 50 | activesupport (= 7.0.4.3) 51 | globalid (>= 0.3.6) 52 | activemodel (7.0.4.3) 53 | activesupport (= 7.0.4.3) 54 | activerecord (7.0.4.3) 55 | activemodel (= 7.0.4.3) 56 | activesupport (= 7.0.4.3) 57 | activestorage (7.0.4.3) 58 | actionpack (= 7.0.4.3) 59 | activejob (= 7.0.4.3) 60 | activerecord (= 7.0.4.3) 61 | activesupport (= 7.0.4.3) 62 | marcel (~> 1.0) 63 | mini_mime (>= 1.1.0) 64 | activesupport (7.0.4.3) 65 | concurrent-ruby (~> 1.0, >= 1.0.2) 66 | i18n (>= 1.6, < 2) 67 | minitest (>= 5.1) 68 | tzinfo (~> 2.0) 69 | addressable (2.8.4) 70 | public_suffix (>= 2.0.2, < 6.0) 71 | archive-zip (0.12.0) 72 | io-like (~> 0.3.0) 73 | bindex (0.8.1) 74 | builder (3.2.4) 75 | byebug (11.1.3) 76 | capybara (3.39.0) 77 | addressable 78 | matrix 79 | mini_mime (>= 0.1.3) 80 | nokogiri (~> 1.8) 81 | rack (>= 1.6.0) 82 | rack-test (>= 0.6.3) 83 | regexp_parser (>= 1.5, < 3.0) 84 | xpath (~> 3.2) 85 | capybara-mechanize (1.13.0) 86 | capybara (>= 3.0.0, < 4) 87 | mechanize (~> 2.8.5) 88 | childprocess (0.9.0) 89 | ffi (~> 1.0, >= 1.0.11) 90 | coderay (1.1.3) 91 | concurrent-ruby (1.2.2) 92 | connection_pool (2.4.0) 93 | crass (1.0.6) 94 | csv (3.2.6) 95 | date (3.3.3) 96 | diff-lcs (1.5.0) 97 | domain_name (0.5.20190701) 98 | unf (>= 0.0.5, < 1.0.0) 99 | dotenv (2.8.1) 100 | dotenv-rails (2.8.1) 101 | dotenv (= 2.8.1) 102 | railties (>= 3.2) 103 | erubi (1.12.0) 104 | ethon (0.16.0) 105 | ffi (>= 1.15.0) 106 | execjs (2.8.1) 107 | ffi (1.15.5) 108 | geckodriver-helper (0.24.0) 109 | archive-zip (~> 0.7) 110 | globalid (1.1.0) 111 | activesupport (>= 5.0) 112 | http-cookie (1.0.5) 113 | domain_name (~> 0.5) 114 | hubspot-api-client (17.0.0.pre.beta.3) 115 | json (~> 2.1, >= 2.1.0) 116 | typhoeus (~> 1.4.0) 117 | i18n (1.12.0) 118 | concurrent-ruby (~> 1.0) 119 | io-like (0.3.1) 120 | jbuilder (2.11.5) 121 | actionview (>= 5.0.0) 122 | activesupport (>= 5.0.0) 123 | json (2.6.3) 124 | listen (3.0.8) 125 | rb-fsevent (~> 0.9, >= 0.9.4) 126 | rb-inotify (~> 0.9, >= 0.9.7) 127 | loofah (2.20.0) 128 | crass (~> 1.0.2) 129 | nokogiri (>= 1.5.9) 130 | mail (2.8.1) 131 | mini_mime (>= 0.1.1) 132 | net-imap 133 | net-pop 134 | net-smtp 135 | marcel (1.0.2) 136 | matrix (0.4.2) 137 | mechanize (2.8.5) 138 | addressable (~> 2.8) 139 | domain_name (~> 0.5, >= 0.5.20190701) 140 | http-cookie (~> 1.0, >= 1.0.3) 141 | mime-types (~> 3.0) 142 | net-http-digest_auth (~> 1.4, >= 1.4.1) 143 | net-http-persistent (>= 2.5.2, < 5.0.dev) 144 | nokogiri (~> 1.11, >= 1.11.2) 145 | rubyntlm (~> 0.6, >= 0.6.3) 146 | webrick (~> 1.7) 147 | webrobots (~> 0.1.2) 148 | method_source (1.0.0) 149 | mime-types (3.4.1) 150 | mime-types-data (~> 3.2015) 151 | mime-types-data (3.2023.0218.1) 152 | mini_mime (1.1.2) 153 | mini_portile2 (2.8.1) 154 | minitest (5.18.0) 155 | net-http-digest_auth (1.4.1) 156 | net-http-persistent (4.0.2) 157 | connection_pool (~> 2.2) 158 | net-imap (0.3.4) 159 | date 160 | net-protocol 161 | net-pop (0.1.2) 162 | net-protocol 163 | net-protocol (0.2.1) 164 | timeout 165 | net-smtp (0.3.3) 166 | net-protocol 167 | nio4r (2.5.9) 168 | nokogiri (1.14.3) 169 | mini_portile2 (~> 2.8.0) 170 | racc (~> 1.4) 171 | pry (0.14.2) 172 | coderay (~> 1.1) 173 | method_source (~> 1.0) 174 | pry-byebug (3.10.1) 175 | byebug (~> 11.0) 176 | pry (>= 0.13, < 0.15) 177 | public_suffix (5.0.1) 178 | puma (4.3.12) 179 | nio4r (~> 2.0) 180 | racc (1.6.2) 181 | rack (2.2.6.4) 182 | rack-test (2.1.0) 183 | rack (>= 1.3) 184 | rails (7.0.4.3) 185 | actioncable (= 7.0.4.3) 186 | actionmailbox (= 7.0.4.3) 187 | actionmailer (= 7.0.4.3) 188 | actionpack (= 7.0.4.3) 189 | actiontext (= 7.0.4.3) 190 | actionview (= 7.0.4.3) 191 | activejob (= 7.0.4.3) 192 | activemodel (= 7.0.4.3) 193 | activerecord (= 7.0.4.3) 194 | activestorage (= 7.0.4.3) 195 | activesupport (= 7.0.4.3) 196 | bundler (>= 1.15.0) 197 | railties (= 7.0.4.3) 198 | rails-dom-testing (2.0.3) 199 | activesupport (>= 4.2.0) 200 | nokogiri (>= 1.6) 201 | rails-html-sanitizer (1.5.0) 202 | loofah (~> 2.19, >= 2.19.1) 203 | railties (7.0.4.3) 204 | actionpack (= 7.0.4.3) 205 | activesupport (= 7.0.4.3) 206 | method_source 207 | rake (>= 12.2) 208 | thor (~> 1.0) 209 | zeitwerk (~> 2.5) 210 | rake (13.0.6) 211 | rb-fsevent (0.11.2) 212 | rb-inotify (0.10.1) 213 | ffi (~> 1.0) 214 | regexp_parser (2.8.0) 215 | rspec-core (3.9.3) 216 | rspec-support (~> 3.9.3) 217 | rspec-expectations (3.9.4) 218 | diff-lcs (>= 1.2.0, < 2.0) 219 | rspec-support (~> 3.9.0) 220 | rspec-mocks (3.9.1) 221 | diff-lcs (>= 1.2.0, < 2.0) 222 | rspec-support (~> 3.9.0) 223 | rspec-rails (3.9.1) 224 | actionpack (>= 3.0) 225 | activesupport (>= 3.0) 226 | railties (>= 3.0) 227 | rspec-core (~> 3.9.0) 228 | rspec-expectations (~> 3.9.0) 229 | rspec-mocks (~> 3.9.0) 230 | rspec-support (~> 3.9.0) 231 | rspec-support (3.9.4) 232 | rubyntlm (0.6.3) 233 | rubyzip (1.3.0) 234 | sass (3.7.4) 235 | sass-listen (~> 4.0.0) 236 | sass-listen (4.0.0) 237 | rb-fsevent (~> 0.9, >= 0.9.4) 238 | rb-inotify (~> 0.9, >= 0.9.7) 239 | sass-rails (5.1.0) 240 | railties (>= 5.2.0) 241 | sass (~> 3.1) 242 | sprockets (>= 2.8, < 4.0) 243 | sprockets-rails (>= 2.0, < 4.0) 244 | tilt (>= 1.1, < 3) 245 | selenium-webdriver (3.4.3) 246 | childprocess (~> 0.5) 247 | rubyzip (~> 1.0) 248 | sprockets (3.7.2) 249 | concurrent-ruby (~> 1.0) 250 | rack (> 1, < 3) 251 | sprockets-rails (3.4.2) 252 | actionpack (>= 5.2) 253 | activesupport (>= 5.2) 254 | sprockets (>= 3.0.0) 255 | sqlite3 (1.6.2) 256 | mini_portile2 (~> 2.8.0) 257 | thor (1.2.1) 258 | tilt (2.1.0) 259 | timeout (0.3.2) 260 | typhoeus (1.4.0) 261 | ethon (>= 0.9.0) 262 | tzinfo (2.0.6) 263 | concurrent-ruby (~> 1.0) 264 | uglifier (4.2.0) 265 | execjs (>= 0.3.0, < 3) 266 | unf (0.1.4) 267 | unf_ext 268 | unf_ext (0.0.8.2) 269 | web-console (4.2.0) 270 | actionview (>= 6.0.0) 271 | activemodel (>= 6.0.0) 272 | bindex (>= 0.4.0) 273 | railties (>= 6.0.0) 274 | webrick (1.8.1) 275 | webrobots (0.1.2) 276 | websocket-driver (0.7.5) 277 | websocket-extensions (>= 0.1.0) 278 | websocket-extensions (0.1.5) 279 | xpath (3.2.0) 280 | nokogiri (~> 1.8) 281 | zeitwerk (2.6.7) 282 | 283 | PLATFORMS 284 | ruby 285 | 286 | DEPENDENCIES 287 | byebug 288 | capybara 289 | capybara-mechanize (~> 1.11) 290 | csv 291 | dotenv-rails 292 | geckodriver-helper 293 | hubspot-api-client 294 | jbuilder (~> 2.5) 295 | listen (>= 3.0.5, < 3.2) 296 | pry 297 | pry-byebug 298 | puma (~> 4.3.5) 299 | rails 300 | rspec-rails (~> 3.5) 301 | sass-rails (~> 5.0) 302 | selenium-webdriver (= 3.4.3) 303 | sqlite3 304 | tzinfo-data 305 | uglifier (>= 1.3.0) 306 | web-console (>= 3.3.0) 307 | 308 | RUBY VERSION 309 | ruby 3.1.3p185 310 | 311 | BUNDLED WITH 312 | 1.17.3 313 | -------------------------------------------------------------------------------- /ruby/README.md: -------------------------------------------------------------------------------- 1 | # HubSpot-ruby sample OAuth 2.0 app 2 | 3 | This is a sample app for the [hubspot-ruby SDK](../../../../) 4 | 5 | Please see the documentation on [How do I create an app in HubSpot?](https://developers.hubspot.com/docs/faq/how-do-i-create-an-app-in-hubspot) 6 | 7 | ### HubSpot Public API links used in this application 8 | 9 | - [Working with OAuth](https://developers.hubspot.com/docs/api/working-with-oauth) 10 | - [Initiate an Integration with OAuth 2.0](https://developers.hubspot.com/docs/methods/oauth2/initiate-oauth-integration) 11 | - [Get OAuth 2.0 Access Token and Refresh Tokens](https://developers.hubspot.com/docs/methods/oauth2/get-access-and-refresh-tokens) 12 | - [Refresh OAuth 2.0 Access Token](https://developers.hubspot.com/docs/methods/oauth2/refresh-access-token) 13 | - [Search contacts](https://developers.hubspot.com/docs-beta/crm/search) 14 | 15 | ### Note on Application Scopes 16 | HubSpot provides a way to restrict application users access to the system to certain scopes. In order to do that it is a good practice to make a set of scopes required by your applicatuion. 17 | Please refer to [Initiate an Integration with OAuth 2.0](https://developers.hubspot.com/docs/methods/oauth2/initiate-oauth-integration) for documentation on the scope parameter passed to https://app.hubspot.com/oauth/authorize to make a set of scopes required. [Scopes](https://developers.hubspot.com/docs/methods/oauth2/initiate-oauth-integration#scopes) explains how to make optional scopes and talks about scopes available in HubSpot system. Needed scopes for this app(`crm.objects.contacts.write, crm.objects.contacts.read`). 18 | 19 | ### Setup App 20 | 21 | Please, make sure that you have ruby 3.1.3 installed and run bundle in your app folder. 22 | 23 | ```bundle install``` 24 | 25 | ### Configure 26 | 27 | 1. Copy .env.template to .env 28 | 2. Paste your HUBSPOT_CLIENT_ID and HUBSPOT_CLIENT_SECRET 29 | 30 | ### Running 31 | 32 | Just run your rails app 33 | 34 | ```rails s``` 35 | 36 | You should now be able to navigate to [http://localhost:3000](http://localhost:3000) and use the application. 37 | -------------------------------------------------------------------------------- /ruby/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /ruby/app/assets/config/manifest.js: -------------------------------------------------------------------------------- 1 | //= link_tree ../images 2 | //= link_directory ../javascripts .js 3 | //= link_directory ../stylesheets .css 4 | -------------------------------------------------------------------------------- /ruby/app/assets/images/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/app/assets/images/.keep -------------------------------------------------------------------------------- /ruby/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's 5 | // vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. JavaScript code in this file should be added after the last require_* statement. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require rails-ujs 14 | //= require activestorage 15 | //= require_tree . 16 | -------------------------------------------------------------------------------- /ruby/app/assets/javascripts/cable.js: -------------------------------------------------------------------------------- 1 | // Action Cable provides the framework to deal with WebSockets in Rails. 2 | // You can generate new channels where WebSocket features live using the `rails generate channel` command. 3 | // 4 | //= require action_cable 5 | //= require_self 6 | //= require_tree ./channels 7 | 8 | (function() { 9 | this.App || (this.App = {}); 10 | 11 | App.cable = ActionCable.createConsumer(); 12 | 13 | }).call(this); 14 | -------------------------------------------------------------------------------- /ruby/app/assets/javascripts/channels/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/app/assets/javascripts/channels/.keep -------------------------------------------------------------------------------- /ruby/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's 6 | * vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS 10 | * files in this directory. Styles in this file should be added after the last require_* statement. 11 | * It is generally better to create a new file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | 17 | .wrapper .container { 18 | padding-bottom: 2rem; 19 | padding-top: 2rem; 20 | } 21 | 22 | .navigation { 23 | background: #f4f5f6; 24 | border-bottom: .1rem solid #d1d1d1; 25 | display: block; 26 | height: 5.2rem; 27 | left: 0; 28 | max-width: 100%; 29 | width: 100%; 30 | } 31 | 32 | .navigation .container { 33 | padding-bottom: 0; 34 | padding-top: 0 35 | } 36 | 37 | .navigation .navigation-list { 38 | list-style: none; 39 | margin-bottom: 0; 40 | } 41 | 42 | .navigation .navigation-item { 43 | float: left; 44 | margin-bottom: 0; 45 | margin-left: 2.5rem; 46 | position: relative 47 | } 48 | 49 | .navigation .navigation-title, .navigation .title { 50 | color: #606c76; 51 | position: relative 52 | } 53 | 54 | .navigation .navigation-link, .navigation .navigation-title, .navigation .title { 55 | display: inline; 56 | font-size: 1.6rem; 57 | line-height: 5.2rem; 58 | padding: 0; 59 | text-decoration: none 60 | } 61 | 62 | .navigation .navigation-link.active { 63 | color: #606c76 64 | } 65 | 66 | .contact-new-button { 67 | margin: 2rem; 68 | margin-left: 0; 69 | } 70 | 71 | .error-wrapper .result { 72 | color: white; 73 | background-color: red; 74 | } 75 | 76 | input[type=datetime-local] { 77 | width: 100%; 78 | height: 3.8rem; 79 | padding: .6rem 1.0rem; 80 | } 81 | 82 | h3.alert-success { 83 | text-align: center; 84 | } 85 | 86 | .delete-button { 87 | margin-bottom: 0; 88 | float: right; 89 | margin-right: 20px; 90 | } 91 | 92 | .delete-button input { 93 | margin-bottom: 0; 94 | } 95 | 96 | .contact a, .property a { 97 | color: #4e585f; 98 | text-decoration: none; 99 | } 100 | 101 | .contact a:hover, .property a:hover { 102 | text-decoration: none; 103 | cursor: pointer; 104 | } 105 | 106 | .contact .contact-info, .property .property-info { 107 | display: block; 108 | } 109 | 110 | pre { 111 | line-height: 0.7; 112 | } 113 | 114 | pre p { 115 | margin: 0; 116 | margin-left: 5px; 117 | } -------------------------------------------------------------------------------- /ruby/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include ExceptionHandler 3 | 4 | before_action :check_env_variables 5 | 6 | private 7 | 8 | def check_env_variables 9 | missing_vars = %w[HUBSPOT_CLIENT_ID HUBSPOT_CLIENT_SECRET].select { |var| ENV[var].blank? } 10 | raise(ExceptionHandler::HubspotError.new, "Please specify #{missing_vars.join(', ')} in .env") if missing_vars.present? 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ruby/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /ruby/app/controllers/concerns/exception_handler.rb: -------------------------------------------------------------------------------- 1 | module ExceptionHandler 2 | extend ActiveSupport::Concern 3 | 4 | class HubspotError < StandardError; end 5 | 6 | included do 7 | rescue_from HubspotError do |error| 8 | @error = error 9 | render template: 'contacts/index' 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /ruby/app/controllers/contacts_controller.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot-api-client' 2 | 3 | class ContactsController < ApplicationController 4 | before_action :authorize 5 | def index 6 | # https://developers.hubspot.com/docs/methods/contacts/get_contacts 7 | @contacts = if params[:search].present? 8 | @search_q = params[:search] 9 | Services::Hubspot::Contacts::Search.new(session[:tokens][:access_token], email: @search_q).call 10 | else 11 | # https://developers.hubspot.com/docs/methods/contacts/get_contacts 12 | Services::Hubspot::Contacts::GetPage.new(limit: 100, access_token: session[:tokens][:access_token]).call.sort_by(&:created_at).reverse 13 | end 14 | end 15 | 16 | def show 17 | @contact = Services::Hubspot::Contacts::GetById.new(params[:id], session[:tokens][:access_token]).call 18 | @owners = Services::Hubspot::Owners::GetAll.new(session[:tokens][:access_token]).call 19 | end 20 | 21 | def create 22 | Services::Hubspot::Contacts::Create.new(session[:tokens][:access_token], email: params[:email]).call 23 | redirect_to :contacts 24 | rescue Hubspot::Crm::Contacts::ApiError => e 25 | error_message = JSON.parse(e.response_body)['message'] 26 | redirect_to new_contact_path, flash: { error: error_message } 27 | end 28 | 29 | def update 30 | @contact = Services::Hubspot::Contacts::GetById.new(params[:id], session[:tokens][:access_token]).call 31 | Services::Hubspot::Contacts::Update.new(session[:tokens][:access_token], params[:id], contact_params).call 32 | redirect_to :contacts 33 | rescue Hubspot::Crm::Contacts::ApiError => e 34 | error_message = JSON.parse(e.response_body)['message'] 35 | redirect_to contact_path(params[:id]), flash: { error: error_message } 36 | end 37 | 38 | def export 39 | respond_to do |format| 40 | format.html 41 | format.csv do 42 | send_data( 43 | Services::Hubspot::Contacts::Export.new(session[:tokens][:access_token]).call, 44 | filename: "contacts-#{Date.today}.csv" 45 | ) 46 | end 47 | end 48 | end 49 | 50 | def destroy 51 | @contact = Services::Hubspot::Contacts::GetById.new(params[:id], session[:tokens][:access_token]).call 52 | Services::Hubspot::Contacts::Destroy.new(params[:id], session[:tokens][:access_token]).call 53 | redirect_back(fallback_location: root_path, notice: "Contact ##{@contact.id} was successfully destroyed.") 54 | end 55 | 56 | private 57 | 58 | def contact_params 59 | params.require(:contact).permit(@contact.properties.keys).to_hash 60 | end 61 | 62 | def authorize 63 | redirect_to login_path and return if session['tokens'].blank? 64 | 65 | session['tokens'] = Services::Authorization::Tokens::Refresh.new(tokens: session['tokens'], request: request).call 66 | Services::Authorization::AuthorizeHubspot.new(tokens: session['tokens']).call 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /ruby/app/controllers/oauth/authorization_controller.rb: -------------------------------------------------------------------------------- 1 | module Oauth 2 | class AuthorizationController < ApplicationController 3 | def authorize 4 | url = Services::Authorization::GetAuthorizationUri.new(request: request).call 5 | redirect_to url 6 | end 7 | 8 | def callback 9 | session[:tokens] = Services::Authorization::Tokens::Generate.new( 10 | code: params[:code], 11 | request: request 12 | ).call 13 | Services::Authorization::AuthorizeHubspot.new(tokens: session[:tokens]).call 14 | redirect_to '/contacts' 15 | end 16 | 17 | def login;end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /ruby/app/lib/services/authorization/authorize_hubspot.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot/client' 2 | 3 | module Services 4 | module Authorization 5 | class AuthorizeHubspot 6 | def initialize(tokens:) 7 | @tokens = tokens.to_hash 8 | end 9 | 10 | def call 11 | ::Hubspot::Client.new(access_token: @tokens[:access_token]) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /ruby/app/lib/services/authorization/get_authorization_uri.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot/oauth_helper' 2 | 3 | module Services 4 | module Authorization 5 | class GetAuthorizationUri 6 | CALLBACK_PATH = '/oauth/callback'.freeze 7 | 8 | def initialize(request:) 9 | @request = request 10 | end 11 | 12 | def call 13 | check_presence_of_credentials 14 | 15 | ::Hubspot::OAuthHelper.authorize_url( 16 | client_id: ENV['HUBSPOT_CLIENT_ID'], 17 | redirect_uri: redirect_uri, 18 | # your registered scopes in the app 19 | scope: %[crm.objects.contacts.read crm.objects.contacts.write] 20 | ) 21 | end 22 | 23 | private 24 | 25 | def redirect_uri 26 | @request.protocol + @request.host_with_port + CALLBACK_PATH 27 | end 28 | 29 | def check_presence_of_credentials 30 | return if ENV['HUBSPOT_CLIENT_ID'].present? 31 | 32 | raise(ExceptionHandler::HubspotError.new, 'Please specify HUBSPOT_CLIENT_ID in .env') 33 | end 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /ruby/app/lib/services/authorization/tokens/base.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Authorization 3 | module Tokens 4 | class Base 5 | CALLBACK_PATH = '/oauth/callback'.freeze 6 | 7 | def expires_at(expires_in) 8 | Time.current + (expires_in * 0.95).round 9 | end 10 | 11 | private 12 | 13 | def redirect_uri 14 | @request.protocol + @request.host_with_port + CALLBACK_PATH 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/app/lib/services/authorization/tokens/generate.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot/client' 2 | 3 | module Services 4 | module Authorization 5 | module Tokens 6 | class Generate < Tokens::Base 7 | def initialize(code:, request:) 8 | @code = code 9 | @request = request 10 | end 11 | 12 | def call 13 | default_api = ::Hubspot::Client.new(api_key: ENV['HUBSPOT_CLIENT_SECRET']) 14 | tokens = default_api.o_auth.tokens_api.create( 15 | grant_type: "authorization_code", 16 | code: @code, 17 | redirect_uri: redirect_uri, 18 | client_id: ENV['HUBSPOT_CLIENT_ID'], 19 | client_secret: ENV['HUBSPOT_CLIENT_SECRET'], 20 | return_type: 'Object' 21 | ) 22 | tokens 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /ruby/app/lib/services/authorization/tokens/refresh.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot/client' 2 | 3 | module Services 4 | module Authorization 5 | module Tokens 6 | class Refresh < Tokens::Base 7 | def initialize(tokens:, request:) 8 | @tokens = tokens.to_hash 9 | @request = request 10 | end 11 | 12 | def call 13 | @tokens = refresh_tokens if Time.current > @tokens[:expires_in] 14 | @tokens 15 | end 16 | 17 | private 18 | 19 | def refresh_tokens 20 | default_api = ::Hubspot::Client.new(api_key: ENV['HUBSPOT_CLIENT_SECRET']) 21 | tokens = default_api.o_auth.tokens_api.create( 22 | grant_type: "refresh_token", 23 | refresh_token: @tokens[:refresh_token], 24 | redirect_uri: redirect_uri, 25 | client_id: ENV['HUBSPOT_CLIENT_ID'], 26 | client_secret: ENV['HUBSPOT_CLIENT_SECRET'], 27 | return_type: 'Object' 28 | ) 29 | hashed_tokens = tokens.to_hash 30 | hashed_tokens[:expires_at] = expires_at(hashed_tokens[:expires_in]) 31 | hashed_tokens 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/create.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class Create 5 | def initialize(access_token, properties) 6 | @access_token = access_token 7 | @properties = properties 8 | end 9 | 10 | def call 11 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 12 | basic_api.crm.contacts.basic_api.create(contact_input) 13 | end 14 | 15 | private 16 | 17 | def contact_input 18 | @contact_input ||= { body:{ properties: @properties } } 19 | end 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/destroy.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class Destroy 5 | def initialize(id, access_token) 6 | @id = id 7 | @access_token = access_token 8 | end 9 | 10 | def call 11 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 12 | basic_api.crm.contacts.basic_api.archive(contact_id: @id) 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/export.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class Export 5 | PROPERTIES_TO_EXPORT = %w[email firstname lastname].freeze 6 | 7 | def initialize(access_token, max_pages: 10) 8 | @access_token = access_token 9 | @max_pages = max_pages 10 | end 11 | 12 | def call 13 | convert_to_csv(contacts) 14 | end 15 | 16 | private 17 | 18 | def contacts 19 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 20 | basic_api.crm.contacts.basic_api.get_all() 21 | end 22 | 23 | def convert_to_csv(contacts, properties: PROPERTIES_TO_EXPORT) 24 | CSV.generate(headers: true) do |csv| 25 | csv << [:id, *properties] 26 | 27 | contacts.each do |contact| 28 | csv << [contact.id, *properties.map { |property| contact.properties[property] }] 29 | end 30 | csv 31 | end 32 | end 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/get_by_id.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class GetById 5 | def initialize(id, access_token) 6 | @id = id 7 | @access_token = access_token 8 | end 9 | 10 | def call 11 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 12 | basic_api.crm.contacts.basic_api.get_by_id( 13 | contact_id: @id, 14 | properties: %w[email firstname lastname hubspot_owner_id] 15 | ) 16 | end 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/get_page.rb: -------------------------------------------------------------------------------- 1 | require 'hubspot-api-client' 2 | 3 | module Services 4 | module Hubspot 5 | module Contacts 6 | class GetPage 7 | def initialize(limit: 10, access_token:) 8 | @limit = limit 9 | @access_token = access_token 10 | end 11 | 12 | def call 13 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 14 | results = basic_api.crm.contacts.basic_api.get_page(limit: @limit).results 15 | add_fullnames(results) 16 | end 17 | 18 | private 19 | 20 | def add_fullnames(contacts) 21 | contacts.each do |contact| 22 | fullname = [contact.properties['firstname'], contact.properties['lastname']].reject(&:empty?).join(' ') 23 | contact.properties['fullname'] = fullname 24 | end 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/search.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class Search 5 | def initialize(access_token, email:) 6 | @access_token = access_token 7 | @email = email 8 | end 9 | 10 | def call 11 | basic_api = ::Hubspot::Client.new(access_token: @access_token) 12 | results = basic_api.crm.contacts.search_api.do_search(body: search_request).results 13 | results = add_fullnames(results) 14 | results 15 | end 16 | 17 | private 18 | 19 | def search_request 20 | { 21 | filterGroups: 22 | [ 23 | { 24 | "filters": 25 | [ 26 | { 27 | propertyName: 'email', 28 | operator: 'EQ', 29 | value: @email 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | end 36 | 37 | def add_fullnames(contacts) 38 | contacts.each do |contact| 39 | fullname = [contact.properties['firstname'], contact.properties['lastname']].reject(&:empty?).join(' ') 40 | contact.properties['fullname'] = fullname 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/contacts/update.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Contacts 4 | class Update 5 | def initialize(access_token, id, properties) 6 | @access_token = access_token 7 | @id = id 8 | @properties = properties 9 | end 10 | 11 | def call 12 | default_api = ::Hubspot::Client.new(access_token: @access_token) 13 | default_api.crm.contacts.basic_api.update(contact_id: @id, body: contact_input) 14 | end 15 | 16 | private 17 | 18 | def contact_input 19 | @contact_input ||= { properties: @properties } 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /ruby/app/lib/services/hubspot/owners/get_all.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | module Hubspot 3 | module Owners 4 | class GetAll 5 | def initialize(access_token) 6 | @access_token = access_token 7 | end 8 | def call 9 | default_api = ::Hubspot::Client.new(access_token: @access_token) 10 | default_api.crm.contacts.basic_api.get_page.results 11 | end 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /ruby/app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /ruby/app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/app/models/concerns/.keep -------------------------------------------------------------------------------- /ruby/app/views/contacts/index.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if @error.present? %> 3 |

<%= @error.message %>

4 | <% else %> 5 |
6 |
 7 |         

Create contact

8 |

# app/lib/services/hubspot/contacts/create.rb

9 | 10 |

Export to CSV

11 |

# app/lib/services/hubspot/contacts/export.rb

12 |
13 | <%= link_to('New Contact', new_contact_path, class: 'button', id: 'new-contact') %> 14 | <%= link_to('Export To CSV', export_contacts_path(format: :csv), class: 'button', id: 'export') %> 15 |
16 | 17 | <%= form_tag('/contacts', method: :get, class: 'contacts') do %> 18 | <%= text_field_tag(:search, @search_q, placeholder: 'Search by email..', id: 'search') %> 19 | <% end %> 20 |
21 |     

Search

22 |

# app/lib/services/hubspot/contacts/search.rb

23 |
24 | 25 | <% if flash[:notice] %> 26 |
<%= flash[:notice] %>
27 | <% end %> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | <% @contacts.each do |contact| %> 40 | 41 | 48 | 55 | 62 | 69 | 70 | <% end %> 71 | 72 |
IDEmailName
42 | 43 |
44 | <%= contact.id %> 45 |
46 |
47 |
49 | 50 |
51 | <%= contact.properties['email'] %> 52 |
53 |
54 |
56 | 57 |
58 | <%= contact.properties['fullname'].present? ? contact.properties['fullname'] : contact.properties['email'] %> 59 |
60 |
61 |
63 | <%= button_to "delete", 64 | { controller: :contacts, action: :destroy, id: contact.id }, 65 | method: :delete, 66 | form_class: 'delete-button' 67 | %> 68 |
73 | <% end %> 74 |
75 |     

Delete

76 |

# app/lib/services/hubspot/contacts/destroy.rb

77 |
78 |
79 | -------------------------------------------------------------------------------- /ruby/app/views/contacts/new.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if flash[:error] %> 3 |
<%= flash[:error] %>
4 | <% end %> 5 | 6 |
7 |
8 |
9 |

Properties

10 | <%= form_tag('/contacts', method: 'post') do %> 11 | <%= label_tag(:email, "Email") %> 12 | <%= text_field_tag(:email, @email) %> 13 | <%= submit_tag("Save") %> 14 | <% end %> 15 |
16 |
17 |
18 |
-------------------------------------------------------------------------------- /ruby/app/views/contacts/show.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <% if flash[:error] %> 3 |
<%= flash[:error] %>
4 | <% end %> 5 |
6 |
7 |
 8 |         

Show

9 |

# app/lib/services/hubspot/contacts/show.rb

10 |
11 |

Properties

12 | <%= form_for :contact, url: "/contacts/#{params[:id]}", method: 'put' do |form| %> 13 | <% @contact.properties.each do |property_key, property_value| %> 14 | <%= label(:contact, property_key, property_key) %> 15 | <%= text_field(:contact, property_key, value: property_value) %> 16 | <% end %> 17 | 18 |
19 |           

Update

20 |

# app/lib/services/hubspot/contacts/update.rb

21 |
22 | <%= submit_tag("Save", class: 'button-primary') %> 23 | <% end %> 24 |
25 |
26 |
27 |
28 |
-------------------------------------------------------------------------------- /ruby/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HubSpot Ruby sample contacts app 6 | <%= csrf_meta_tags %> 7 | <%= csp_meta_tag %> 8 | 9 | 10 | 11 | 12 | 13 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> 14 | <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %> 15 | 16 | 17 | 18 | <%= render partial: "shared/header"%> 19 |
20 | <%= yield %> 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /ruby/app/views/oauth/authorization/login.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
 3 |     

// app/lib/services/authorization/get_authorization_uri.rb - Generate URL for OAuth

4 | 5 |

::Hubspot::OAuthHelper.authorize_url(

6 |

client_id: 'client_id',

7 |

redirect_uri: redirect_uri,

8 |

scope: %w[contacts]

9 |

)

10 |
11 |

In order to continue please authorize via OAuth

12 |
13 | Authorize 14 |
15 |
-------------------------------------------------------------------------------- /ruby/app/views/shared/_header.html.erb: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /ruby/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /ruby/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /ruby/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /ruby/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a starting point to setup your application. 14 | # Add necessary setup steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | # puts "\n== Copying sample files ==" 24 | # unless File.exist?('config/database.yml') 25 | # cp 'config/database.yml.sample', 'config/database.yml' 26 | # end 27 | 28 | puts "\n== Preparing database ==" 29 | system! 'bin/rails db:setup' 30 | 31 | puts "\n== Removing old logs and tempfiles ==" 32 | system! 'bin/rails log:clear tmp:clear' 33 | 34 | puts "\n== Restarting application server ==" 35 | system! 'bin/rails restart' 36 | end 37 | -------------------------------------------------------------------------------- /ruby/bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads Spring without using Bundler, in order to be fast. 4 | # It gets overwritten when you run the `spring binstub` command. 5 | 6 | unless defined?(Spring) 7 | require 'rubygems' 8 | require 'bundler' 9 | 10 | lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) 11 | spring = lockfile.specs.detect { |spec| spec.name == 'spring' } 12 | if spring 13 | Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path 14 | gem 'spring', spring.version 15 | require 'spring/binstub' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /ruby/bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'fileutils' 3 | include FileUtils 4 | 5 | # path to your application root. 6 | APP_ROOT = File.expand_path('..', __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts '== Installing dependencies ==' 17 | system! 'gem install bundler --conservative' 18 | system('bundle check') || system!('bundle install') 19 | 20 | # Install JavaScript dependencies if using Yarn 21 | # system('bin/yarn') 22 | 23 | puts "\n== Updating database ==" 24 | system! 'bin/rails db:migrate' 25 | 26 | puts "\n== Removing old logs and tempfiles ==" 27 | system! 'bin/rails log:clear tmp:clear' 28 | 29 | puts "\n== Restarting application server ==" 30 | system! 'bin/rails restart' 31 | end 32 | -------------------------------------------------------------------------------- /ruby/bin/yarn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_ROOT = File.expand_path('..', __dir__) 3 | Dir.chdir(APP_ROOT) do 4 | begin 5 | exec "yarnpkg", *ARGV 6 | rescue Errno::ENOENT 7 | $stderr.puts "Yarn executable was not detected in the system." 8 | $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" 9 | exit 1 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /ruby/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /ruby/config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require 'rails/all' 4 | 5 | # Require the gems listed in Gemfile, including any gems 6 | # you've limited to :test, :development, or :production. 7 | Bundler.require(*Rails.groups) 8 | 9 | module Oauth2App 10 | class Application < Rails::Application 11 | # Initialize configuration defaults for originally generated Rails version. 12 | config.load_defaults 5.2 13 | 14 | # Settings in config/environments/* take precedence over those specified here. 15 | # Application configuration can go into files in config/initializers 16 | # -- all .rb files in that directory are automatically loaded after loading 17 | # the framework and any gems in your application. 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /ruby/config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /ruby/config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> 10 | channel_prefix: contacts-app_production 11 | -------------------------------------------------------------------------------- /ruby/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/production.sqlite3 26 | -------------------------------------------------------------------------------- /ruby/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /ruby/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | # Run rails dev:cache to toggle caching. 17 | if Rails.root.join('tmp', 'caching-dev.txt').exist? 18 | config.action_controller.perform_caching = true 19 | 20 | config.cache_store = :memory_store 21 | config.public_file_server.headers = { 22 | 'Cache-Control' => "public, max-age=#{2.days.to_i}" 23 | } 24 | else 25 | config.action_controller.perform_caching = false 26 | 27 | config.cache_store = :null_store 28 | end 29 | 30 | # Store uploaded files on the local file system (see config/storage.yml for options) 31 | config.active_storage.service = :local 32 | 33 | # Don't care if the mailer can't send. 34 | config.action_mailer.raise_delivery_errors = false 35 | 36 | config.action_mailer.perform_caching = false 37 | 38 | # Print deprecation notices to the Rails logger. 39 | config.active_support.deprecation = :log 40 | 41 | # Raise an error on page load if there are pending migrations. 42 | config.active_record.migration_error = :page_load 43 | 44 | # Highlight code that triggered database queries in logs. 45 | config.active_record.verbose_query_logs = true 46 | 47 | # Debug mode disables concatenation and preprocessing of assets. 48 | # This option may cause significant delays in view rendering with a large 49 | # number of complex assets. 50 | config.assets.debug = true 51 | 52 | # Suppress logger output for asset requests. 53 | config.assets.quiet = true 54 | 55 | # Raises error for missing translations 56 | # config.action_view.raise_on_missing_translations = true 57 | 58 | # Use an evented file watcher to asynchronously detect changes in source code, 59 | # routes, locales, etc. This feature depends on the listen gem. 60 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 61 | end 62 | -------------------------------------------------------------------------------- /ruby/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] 18 | # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). 19 | # config.require_master_key = true 20 | 21 | # Disable serving static files from the `/public` folder by default since 22 | # Apache or NGINX already handles this. 23 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 24 | 25 | # Compress JavaScripts and CSS. 26 | config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 33 | 34 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 35 | # config.action_controller.asset_host = 'http://assets.example.com' 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 40 | 41 | # Store uploaded files on the local file system (see config/storage.yml for options) 42 | config.active_storage.service = :local 43 | 44 | # Mount Action Cable outside main process or domain 45 | # config.action_cable.mount_path = nil 46 | # config.action_cable.url = 'wss://example.com/cable' 47 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 48 | 49 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 50 | # config.force_ssl = true 51 | 52 | # Use the lowest log level to ensure availability of diagnostic information 53 | # when problems arise. 54 | config.log_level = :debug 55 | 56 | # Prepend all log lines with the following tags. 57 | config.log_tags = [ :request_id ] 58 | 59 | # Use a different cache store in production. 60 | # config.cache_store = :mem_cache_store 61 | 62 | # Use a real queuing backend for Active Job (and separate queues per environment) 63 | # config.active_job.queue_adapter = :resque 64 | # config.active_job.queue_name_prefix = "contacts-app_#{Rails.env}" 65 | 66 | config.action_mailer.perform_caching = false 67 | 68 | # Ignore bad email addresses and do not raise email delivery errors. 69 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 70 | # config.action_mailer.raise_delivery_errors = false 71 | 72 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 73 | # the I18n.default_locale when a translation cannot be found). 74 | config.i18n.fallbacks = true 75 | 76 | # Send deprecation notices to registered listeners. 77 | config.active_support.deprecation = :notify 78 | 79 | # Use default logging formatter so that PID and timestamp are not suppressed. 80 | config.log_formatter = ::Logger::Formatter.new 81 | 82 | # Use a different logger for distributed setups. 83 | # require 'syslog/logger' 84 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 85 | 86 | if ENV["RAILS_LOG_TO_STDOUT"].present? 87 | logger = ActiveSupport::Logger.new(STDOUT) 88 | logger.formatter = config.log_formatter 89 | config.logger = ActiveSupport::TaggedLogging.new(logger) 90 | end 91 | 92 | # Do not dump schema after migrations. 93 | config.active_record.dump_schema_after_migration = false 94 | end 95 | -------------------------------------------------------------------------------- /ruby/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => "public, max-age=#{1.hour.to_i}" 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | 31 | # Store uploaded files on the local file system in a temporary directory 32 | config.active_storage.service = :test 33 | 34 | config.action_mailer.perform_caching = false 35 | 36 | # Tell Action Mailer not to deliver emails to the real world. 37 | # The :test delivery method accumulates sent emails in the 38 | # ActionMailer::Base.deliveries array. 39 | config.action_mailer.delivery_method = :test 40 | 41 | # Print deprecation notices to the stderr. 42 | config.active_support.deprecation = :stderr 43 | 44 | # Raises error for missing translations 45 | # config.action_view.raise_on_missing_translations = true 46 | end 47 | -------------------------------------------------------------------------------- /ruby/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | # Add Yarn node_modules folder to the asset load path. 9 | Rails.application.config.assets.paths << Rails.root.join('node_modules') 10 | 11 | # Precompile additional assets. 12 | # application.js, application.css, and all non-JS/CSS in the app/assets 13 | # folder are already added. 14 | # Rails.application.config.assets.precompile += %w( admin.js admin.css ) 15 | -------------------------------------------------------------------------------- /ruby/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /ruby/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | Mime::Type.register "text/csv", :csv -------------------------------------------------------------------------------- /ruby/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /ruby/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # The following keys must be escaped otherwise they will not be retrieved by 20 | # the default I18n backend: 21 | # 22 | # true, false, on, off, yes, no 23 | # 24 | # Instead, surround them with single quotes. 25 | # 26 | # en: 27 | # 'true': 'foo' 28 | # 29 | # To learn more, please read the Rails Internationalization guide 30 | # available at http://guides.rubyonrails.org/i18n.html. 31 | 32 | en: 33 | hello: "Hello world" 34 | -------------------------------------------------------------------------------- /ruby/config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers: a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum; this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests; default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. 30 | # 31 | # preload_app! 32 | 33 | # Allow puma to be restarted by `rails restart` command. 34 | plugin :tmp_restart 35 | -------------------------------------------------------------------------------- /ruby/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :contacts do 3 | collection do 4 | get :export 5 | end 6 | end 7 | 8 | resources :properties 9 | 10 | get '/oauth', to: 'oauth/authorization#authorize' 11 | get '/oauth/callback', to: 'oauth/authorization#callback' 12 | get '/login', to: 'oauth/authorization#login' 13 | root to: 'contacts#index' 14 | end 15 | -------------------------------------------------------------------------------- /ruby/config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /ruby/db/seeds.rb: -------------------------------------------------------------------------------- 1 | # This file should contain all the record creation needed to seed the database with its default values. 2 | # The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup). 3 | # 4 | # Examples: 5 | # 6 | # movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }]) 7 | # Character.create(name: 'Luke', movie: movies.first) 8 | -------------------------------------------------------------------------------- /ruby/lib/assets/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/lib/assets/.keep -------------------------------------------------------------------------------- /ruby/lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/lib/tasks/.keep -------------------------------------------------------------------------------- /ruby/log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/log/.keep -------------------------------------------------------------------------------- /ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contacts-app", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /ruby/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /ruby/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /ruby/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /ruby/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /ruby/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/public/apple-touch-icon.png -------------------------------------------------------------------------------- /ruby/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/public/favicon.ico -------------------------------------------------------------------------------- /ruby/public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | -------------------------------------------------------------------------------- /ruby/spec/features/visitor_creates_new_contact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Visitor creates new contact' do 4 | let(:email) { 'test_email@q.com' } 5 | let(:incorrect_email) { 'wrong_email.com' } 6 | 7 | before do 8 | login 9 | end 10 | 11 | scenario 'with correct data' do 12 | using_wait_time 10 do 13 | create_contact(email) 14 | 15 | expect(page).to have_css('.contacts td', text: email) 16 | remove_test_contact(email) 17 | end 18 | end 19 | 20 | scenario 'with incorrect data' do 21 | using_wait_time 10 do 22 | create_contact(incorrect_email) 23 | 24 | expect(page).to have_css('.error', text: 'Property values were not valid') 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_deletes_contact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Deleting contact' do 4 | let(:email) { 'test_email@q.com' } 5 | 6 | scenario 'Visitor deletes contact' do 7 | login 8 | using_wait_time 10 do 9 | create_contact(email) 10 | expect(page).to have_css('.contacts td', text: email) 11 | find('.contacts .contact-email', text: email) 12 | .find(:xpath, 'ancestor::tr[@class="contact"]') 13 | .find('.delete-button').click 14 | 15 | expect(page).not_to have_css('.contacts td', text: email) 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_downloads_contacts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Downloading CSV' do 4 | let(:email) { 'test_email@q.com' } 5 | 6 | before do 7 | login 8 | using_wait_time 10 do 9 | create_contact(email) 10 | end 11 | end 12 | 13 | after do 14 | remove_test_contact(email) 15 | end 16 | 17 | scenario 'visitor downloads csv' do 18 | click_on('Export To CSV') 19 | expect( DownloadHelper::download_content ).to include(email) 20 | end 21 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_edits_contact_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'editing of contact' do 4 | let(:email) { 'test_email@q.com' } 5 | let(:new_email) { 'new_test_email@q.com' } 6 | let(:firstname) { 'test_firstname' } 7 | let(:lastname) { 'test_lastname' } 8 | 9 | before do 10 | login 11 | using_wait_time 10 do 12 | create_contact(email) 13 | find('.contacts .contact-email', text: email).click 14 | end 15 | end 16 | 17 | after do 18 | visit root_path 19 | remove_test_contact(email) if has_css?('.contacts .contact-email', text: email) 20 | end 21 | 22 | scenario 'edits email' do 23 | find("input[id$='contact_email']").fill_in with: new_email 24 | find("input[type='submit']").click 25 | expect(page).to have_css('.contacts .contact-email', text: new_email) 26 | remove_test_contact(new_email) 27 | end 28 | 29 | scenario 'edits name' do 30 | find("input[id$='contact_firstname']").fill_in with: firstname 31 | find("input[id$='contact_lastname']").fill_in with: lastname 32 | find("input[type='submit']").click 33 | expect(page).to have_css('.contacts .contact-fullname', text: [firstname, lastname].join(' ')) 34 | end 35 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_lists_contacts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Visitor lists contacts' do 4 | scenario 'after login' do 5 | login 6 | using_wait_time 10 do 7 | expect(page).to have_css('.contacts') 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_logs_in_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Visitor logs in' do 4 | scenario 'without HUBSPOT_CLIENT_ID and HUBSPOT_CLIENT_SECRET' do 5 | allow(ENV).to receive(:[]) 6 | allow(ENV).to receive(:[]).with('HUBSPOT_CLIENT_ID').and_return(nil) 7 | allow(ENV).to receive(:[]).with('HUBSPOT_CLIENT_SECRET').and_return(nil) 8 | 9 | visit root_path 10 | expect(page).to have_content('Please specify HUBSPOT_CLIENT_ID, HUBSPOT_CLIENT_SECRET in .env') 11 | end 12 | 13 | scenario 'without creds in session' do 14 | visit root_path 15 | expect(page).to have_content('In order to continue please authorize via OAuth') 16 | end 17 | 18 | scenario 'clicking on OAuth2 link' do 19 | login 20 | expect(page).to have_css('.contacts') 21 | end 22 | end -------------------------------------------------------------------------------- /ruby/spec/features/visitor_searches_contacts_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | feature 'Visitor searches contacts' do 4 | let(:email) { 'test_email@q.com' } 5 | let(:wrong_email) { 'wrong_email.com' } 6 | 7 | before do 8 | login 9 | using_wait_time 10 do 10 | create_contact(email) 11 | end 12 | end 13 | 14 | after do 15 | remove_test_contact(email) 16 | end 17 | 18 | scenario 'with correct query' do 19 | using_wait_time 10 do 20 | find("input[id$='search']").set(email).native.send_keys(:return) 21 | expect(page).to have_css('.contacts td', text: email) 22 | end 23 | end 24 | 25 | scenario 'with correct query' do 26 | using_wait_time 10 do 27 | search_input = find("input[id$='search']") 28 | search_input.set(wrong_email).native.send_keys(:return) 29 | expect(page).not_to have_css('.contacts td', text: email) 30 | search_input.set('').native.send_keys(:return) 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /ruby/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | require 'spec_helper' 3 | ENV['RAILS_ENV'] ||= 'test' 4 | RSPEC_ROOT = File.dirname(__FILE__) 5 | 6 | require File.expand_path('../config/environment', __dir__) 7 | abort("The Rails environment is running in production mode!") if Rails.env.production? 8 | 9 | require 'rspec/rails' 10 | require 'capybara/rspec' 11 | require 'capybara/mechanize' 12 | 13 | Dir["#{RSPEC_ROOT}/support/**/*.rb"].each { |f| require f } 14 | 15 | begin 16 | ActiveRecord::Migration.maintain_test_schema! 17 | rescue ActiveRecord::PendingMigrationError => e 18 | puts e.to_s.strip 19 | exit 1 20 | end 21 | 22 | RSpec.configure do |config| 23 | config.infer_spec_type_from_file_location! 24 | config.filter_rails_from_backtrace! 25 | config.use_transactional_fixtures = false 26 | 27 | config.include ApplicationHelper, DownloadHelper 28 | 29 | Capybara.register_driver :selenium do |app| 30 | profile = Selenium::WebDriver::Firefox::Profile.new 31 | profile['browser.download.dir'] = DownloadHelper::PATH.to_s 32 | profile['browser.download.folderList'] = 2 33 | 34 | # Suppress "open with" dialog 35 | profile['browser.helperApps.neverAsk.saveToDisk'] = 'text/csv' 36 | Capybara::Selenium::Driver.new(app, :browser => :firefox, :profile => profile) 37 | end 38 | 39 | config.before( :each ) do 40 | DownloadHelper::clear_downloads 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /ruby/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.expect_with :rspec do |expectations| 3 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 4 | end 5 | 6 | config.mock_with :rspec do |mocks| 7 | mocks.verify_partial_doubles = true 8 | end 9 | 10 | config.shared_context_metadata_behavior = :apply_to_host_groups 11 | end 12 | -------------------------------------------------------------------------------- /ruby/spec/support/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | def login 3 | Capybara.server_host = "localhost" 4 | Capybara.default_driver = :selenium 5 | 6 | visit root_path 7 | click_link 'OAuth2' 8 | using_wait_time 10 do 9 | unless has_css?("a[href*=\"oauth/#{ENV['HUBSPOT_USER_ID']}/authorize\"]") 10 | find("input[id$='username']").fill_in with: ENV['HUBSPOT_EMAIL'] 11 | find("input[id$='password']").fill_in with: ENV['HUBSPOT_PASSWORD'] 12 | find("button[id$='loginBtn']").click 13 | end 14 | 15 | find("a[href*=\"oauth/#{ENV['HUBSPOT_USER_ID']}/authorize\"]").first('span').click 16 | end 17 | end 18 | 19 | def remove_test_contact(email) 20 | id = find('.contacts .contact-email', text: email).find(:xpath, 'ancestor::tr[@class="contact"]').find('.contact-id').text 21 | Services::Hubspot::Contacts::Destroy.new(id).call 22 | end 23 | 24 | def create_contact(email) 25 | find("a[id$='new-contact']").click 26 | find("input[id$='email']").fill_in with: email 27 | find("input[type='submit']").click 28 | end 29 | end -------------------------------------------------------------------------------- /ruby/spec/support/download_helper.rb: -------------------------------------------------------------------------------- 1 | module DownloadHelper 2 | TIMEOUT = 1 3 | PATH = Rails.root.join("tmp/downloads") 4 | 5 | extend self 6 | 7 | def downloads 8 | Dir[PATH.join("*")] 9 | end 10 | 11 | def download 12 | downloads.first 13 | end 14 | 15 | def download_content 16 | wait_for_download 17 | File.read(download) 18 | end 19 | 20 | def wait_for_download 21 | Timeout.timeout(TIMEOUT) do 22 | sleep 0.1 until downloaded? 23 | end 24 | end 25 | 26 | def downloaded? 27 | !downloading? && downloads.any? 28 | end 29 | 30 | def downloading? 31 | downloads.grep(/\.part$/).any? 32 | end 33 | 34 | def clear_downloads 35 | FileUtils.rm_f(downloads) 36 | end 37 | end -------------------------------------------------------------------------------- /ruby/tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HubSpot/sample-apps-oauth/896d7a5a9ff080a082a2c836d5f86a03b4c230ae/ruby/tmp/.keep --------------------------------------------------------------------------------