40 |
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2019-present, BigCommerce Pty. Ltd. All rights reserved
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
6 | persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
9 | Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "BigCommerce Sample App, Python",
3 | "description": "Sample app for the BigCommerce app store. Handles installation flow and runs an API request, uses the response data to render an interface.",
4 | "keywords": [
5 | "bigcommerce",
6 | "ecommerce",
7 | "python",
8 | "flask"
9 | ],
10 | "repository": "https://github.com/bigcommerce/hello-world-app-python-flask",
11 | "logo": "https://i.imgur.com/PEkdHpr.png",
12 | "success_url": "/instructions",
13 | "scripts": {
14 | "postdeploy": "python -c 'from app import db; db.create_all()'"
15 | },
16 | "env": {
17 | "SESSION_SECRET": {
18 | "description": "Random string used to secure the flask session cookie.",
19 | "generator": "secret"
20 | },
21 | "APP_URL": {
22 | "description": "The public URL of your Heroku app.",
23 | "value": "This will be replaced after deployment"
24 | },
25 | "DEBUG": {
26 | "description": "Boolean used to put Flask, SQLAlchemy, and other addons into a verbose logging and debug mode. Leave it True for now, but be sure to set it to False for a production app release!",
27 | "value": "True"
28 | },
29 | "APP_CLIENT_ID": {
30 | "description": "Replace this with the BigCommerce Client ID",
31 | "value": "Client ID"
32 | },
33 | "APP_CLIENT_SECRET": {
34 | "description": "Replace this with the BigCommerce Client Secret",
35 | "value": "Client Secret"
36 | }
37 | },
38 | "formation": {
39 | "web": {
40 | "quantity": 1,
41 | "size": "free"
42 | }
43 | },
44 | "image": "heroku/python",
45 | "addons": [
46 | "heroku-postgresql"
47 | ]
48 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the Hello World apps
2 |
3 | Thanks for showing interest in contributing!
4 |
5 | The following is a set of guidelines for contributing to the Hello World app. These are just guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
6 |
7 | #### Table of Contents
8 |
9 | [API Documentation](https://developer.bigcommerce.com/api)
10 |
11 | [How Can I Contribute?](#how-can-i-contribute)
12 | * [Your First Code Contribution](#your-first-code-contribution)
13 | * [Pull Requests](#pull-requests)
14 |
15 | [Styleguides](#styleguides)
16 | * [Git Commit Messages](#git-commit-messages)
17 | * [Python Styleguide](#python-styleguide)
18 |
19 | ### Your First Code Contribution
20 |
21 | Unsure where to begin contributing to Hello World app? Check our [forums](https://forum.bigcommerce.com/s/group/0F913000000HLjECAW), our [stackoverflow](https://stackoverflow.com/questions/tagged/bigcommerce) tag, and the reported [issues](https://github.com/bigcommerce/hello-world-app-python-flask/issues).
22 |
23 | ### Pull Requests
24 |
25 | * Fill in [the required template](https://github.com/bigcommerce/hello-world-app-python-flask/pull/new/master)
26 | * Include screenshots and animated GIFs in your pull request whenever possible.
27 | * End files with a newline.
28 |
29 | ## Styleguides
30 |
31 | ### Git Commit Messages
32 |
33 | * Use the present tense ("Add feature" not "Added feature")
34 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
35 | * Limit the first line to 72 characters or less
36 | * Reference pull requests and external links liberally
37 |
38 | ### Python Styleguide
39 |
40 | All Python must adhere to [PEP8 Python Code Styleguide](https://www.python.org/dev/peps/pep-0008/).
41 |
42 |
--------------------------------------------------------------------------------
/templates/instructions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
Heroku deployment successful
10 |
Final steps
11 |
Update app in BC dev portal
12 |
To complete setup, please fill in these values in the dev portal:
13 |
14 |
Callback URL:
15 |
Load URL:
16 |
Uninstall URL:
17 |
Remove User URL:
18 |
19 |
Update Heroku environment variable
20 |
One last step! You'll need to log into your Heroku account and set the APP_URL environment variable to:
21 |
If you have the Heroku CLI, you can just run this command:
22 |
You can view Heroku's documentation on setting environment variables here.
24 |
25 |
26 |
36 |
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://heroku.com/deploy) [(Heroku instructions)](#getting-started-heroku-version)
2 | # BigCommerce Sample App: Python
3 | This is a small Flask application that implements the OAuth callback flow for BigCommerce [Single Click Apps][single_click_apps]
4 | and uses the [BigCommerce API][api_client] to pull a list of products on a BigCommerce store. For information on how to develop apps
5 | for BigCommerce stores, see our [Developer Portal][devportal].
6 |
7 | We hope this sample gives you a good starting point for building your next killer app! What follows are steps specific
8 | to running and installing this sample application.
9 |
10 | ### Registering the app with BigCommerce
11 | 1. Create a trial store on [BigCommerce](https://www.bigcommerce.com/)
12 | 2. Go to the [Developer Portal][devportal] and log in by going to "My Apps"
13 | 3. Click the button "Create an app", enter a name for the new app, and then click "Create"
14 | 4. You don't have to fill out all the details for your app right away, but you do need
15 | to provide some core details in section 4 (Technical). Note that if you are just getting
16 | started, you can use `localhost` for your hostname, but ultimately you'll need to host your
17 | app on the public Internet.
18 | * _Auth Callback URL_: `https:///bigcommerce/callback`
19 | * _Load Callback URL_: `https:///bigcommerce/load`
20 | * _Uninstall Callback URL_: `https:///bigcommerce/uninstall`
21 | * _Remove User Callback URL_: `https:///bigcommerce/remove-user` (if enabling your app for multiple users)
22 | 5. Enable the _Products - Read Only_ scope under _OAuth scopes_, which is what this sample app needs.
23 | 6. Click `Save & Close` on the top right of the dialog.
24 | 7. You'll now see your app in a list in the _My Apps_ section of Developer Portal. Hover over it and click
25 | _View Client ID_. You'll need these values in the next step.
26 |
27 | ### Getting started
28 | 1. Clone this repo: `git clone git@github.com:bigcommerce/hello-world-app-python-flask.git`
29 | 2. Change to the repo directory: `cd hello-world-app-python-flask`
30 | 3. If you want to use virtualenv: `virtualenv ENV && source ENV/bin/activate`
31 | 4. Install dependencies with pip: `pip install -r requirements.txt`
32 | 5. Copy `.env-example` to `.env`
33 | 6. Edit `.env`:
34 | * Set `BC_CLIENT_ID` and `BC_CLIENT_SECRET` to the values obtained from Developer Portal.
35 | * Set `APP_URL` to `https://`.
36 | * Set `SESSION_SECRET` to a long random string, such as that generated by `os.urandom(64)`.
37 | 7. Make sure to populate the database by opening a Python shell from within the app and running
38 | ```
39 | from app import db
40 | db.create_all()
41 | ```
42 | 7. Run the app: `python ./app.py`
43 | 8. Then follow the steps under Installing the app in your trial store.
44 |
45 | ### Hosting the app
46 | In order to install this app in a BigCommerce store, it must be hosted on the public Internet. You can get started in development
47 | by simply running `python app.py` to run it locally, and then use `localhost` in your URLs, but ultimately you will need to host
48 | it somewhere to use the app anywhere other than your development system.
49 |
50 | ### Getting started (Heroku version)
51 |
52 | 1. Click this button: [](https://heroku.com/deploy)
53 | 2. Fill in the details from the app portal on the Heroku deployment page
54 | * See [Registering the app with BigCommerce](#registering-the-app-with-bigcommerce) above. Ignore the callback URLs, just save the app to get the Client ID and Client Secret.
55 | 3. Deploy the app, and click "view" when it's done
56 | 4. Take the callback URLs from the instructions page and plug them into the dev portal.
57 | 5. Then follow the steps under Installing the app in your trial store.
58 |
59 | ### Installing the app in your trial store
60 | * Login to your trial store
61 | * Go to the Marketplace and click _My Drafts_. Find the app you just created and click it.
62 | * A details dialog will open. Click _Install_ and the draft app will be installed in your store.
63 |
64 | [single_click_apps]: https://developer.bigcommerce.com/api/#building-oauth-apps
65 | [api_client]: https://pypi.python.org/pypi/bigcommerce
66 | [devportal]: https://developer.bigcommerce.com
67 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | from bigcommerce.api import BigcommerceApi
2 | import dotenv
3 | import flask
4 | from flask_sqlalchemy import SQLAlchemy
5 | from sqlalchemy.orm import relationship
6 | import os
7 |
8 | # do __name__.split('.')[0] if initialising from a file not at project root
9 | app = flask.Flask(__name__)
10 |
11 | # Look for a .env file
12 | if os.path.exists('.env'):
13 | dotenv.load_dotenv('.env')
14 |
15 | # Load configuration from environment, with defaults
16 | app.config['DEBUG'] = True if os.getenv('DEBUG') == 'True' else False
17 | app.config['LISTEN_HOST'] = os.getenv('LISTEN_HOST', '0.0.0.0')
18 | app.config['LISTEN_PORT'] = int(os.getenv('LISTEN_PORT', '5000'))
19 | app.config['APP_URL'] = os.getenv('APP_URL', 'http://localhost:5000') # must be https to avoid browser issues
20 | app.config['APP_CLIENT_ID'] = os.getenv('APP_CLIENT_ID')
21 | app.config['APP_CLIENT_SECRET'] = os.getenv('APP_CLIENT_SECRET')
22 | app.config['SESSION_SECRET'] = os.getenv('SESSION_SECRET', os.urandom(64))
23 | app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'sqlite:///data/hello_world.sqlite').replace("postgres://", "postgresql://", 1)
24 | app.config['SQLALCHEMY_ECHO'] = app.config['DEBUG']
25 | app.config['SESSION_COOKIE_SAMESITE'] = "None"
26 | app.config['SESSION_COOKIE_SECURE'] = True
27 |
28 | # Setup secure cookie secret
29 | app.secret_key = app.config['SESSION_SECRET']
30 |
31 | # Setup db
32 | db = SQLAlchemy(app)
33 |
34 |
35 | class User(db.Model):
36 | id = db.Column(db.Integer, primary_key=True)
37 | bc_id = db.Column(db.Integer, nullable=False)
38 | email = db.Column(db.String(120), nullable=False)
39 | storeusers = relationship("StoreUser", backref="user")
40 |
41 | def __init__(self, bc_id, email):
42 | self.bc_id = bc_id
43 | self.email = email
44 |
45 | def __repr__(self):
46 | return '' % (self.id, self.bc_id, self.email)
47 |
48 |
49 | class StoreUser(db.Model):
50 | id = db.Column(db.Integer, primary_key=True)
51 | store_id = db.Column(db.Integer, db.ForeignKey('store.id'), nullable=False)
52 | user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
53 | admin = db.Column(db.Boolean, nullable=False, default=False)
54 |
55 | def __init__(self, store, user, admin=False):
56 | self.store_id = store.id
57 | self.user_id = user.id
58 | self.admin = admin
59 |
60 | def __repr__(self):
61 | return '' \
62 | % (self.id, self.user.email, self.user_id, self.store.store_id, self.admin)
63 |
64 |
65 | class Store(db.Model):
66 | id = db.Column(db.Integer, primary_key=True)
67 | store_hash = db.Column(db.String(16), nullable=False, unique=True)
68 | access_token = db.Column(db.String(128), nullable=False)
69 | scope = db.Column(db.Text(), nullable=False)
70 | admin_storeuser_id = relationship("StoreUser",
71 | primaryjoin="and_(StoreUser.store_id==Store.id, StoreUser.admin==True)")
72 | storeusers = relationship("StoreUser", backref="store")
73 |
74 | def __init__(self, store_hash, access_token, scope):
75 | self.store_hash = store_hash
76 | self.access_token = access_token
77 | self.scope = scope
78 |
79 | def __repr__(self):
80 | return '' \
81 | % (self.id, self.store_hash, self.access_token, self.scope)
82 |
83 |
84 | #
85 | # Error handling and helpers
86 | #
87 | def error_info(e):
88 | content = ""
89 | try: # it's probably a HttpException, if you're using the bigcommerce client
90 | content += str(e.headers) + " " + str(e.content) + " "
91 | req = e.response.request
92 | content += " Request: " + req.url + " " + str(req.headers) + " " + str(req.body)
93 | except AttributeError as e: # not a HttpException
94 | content += "
(This page threw an exception: {})".format(str(e))
95 | return content
96 |
97 |
98 | @app.errorhandler(500)
99 | def internal_server_error(e):
100 | content = "Internal Server Error: " + str(e) + " "
101 | content += error_info(e)
102 | return content, 500
103 |
104 |
105 | @app.errorhandler(400)
106 | def bad_request(e):
107 | content = "Bad Request: " + str(e) + " "
108 | content += error_info(e)
109 | return content, 400
110 |
111 |
112 | def jwt_error(e):
113 | print(f"JWT verification failed: {e}")
114 | return "Payload verification failed!", 401
115 |
116 |
117 | # Helper for template rendering
118 | def render(template, context):
119 | return flask.render_template(template, **context)
120 |
121 |
122 | def client_id():
123 | return app.config['APP_CLIENT_ID']
124 |
125 |
126 | def client_secret():
127 | return app.config['APP_CLIENT_SECRET']
128 |
129 | #
130 | # OAuth pages
131 | #
132 |
133 |
134 | # The Auth Callback URL. See https://developer.bigcommerce.com/api/callback
135 | @app.route('/bigcommerce/callback')
136 | def auth_callback():
137 | # Put together params for token request
138 | code = flask.request.args['code']
139 | context = flask.request.args['context']
140 | scope = flask.request.args['scope']
141 | store_hash = context.split('/')[1]
142 | redirect = app.config['APP_URL'] + flask.url_for('auth_callback')
143 |
144 | # Fetch a permanent oauth token. This will throw an exception on error,
145 | # which will get caught by our error handler above.
146 | client = BigcommerceApi(client_id=client_id(), store_hash=store_hash)
147 | token = client.oauth_fetch_token(client_secret(), code, context, scope, redirect)
148 | bc_user_id = token['user']['id']
149 | email = token['user']['email']
150 | access_token = token['access_token']
151 |
152 | # Create or update store
153 | store = Store.query.filter_by(store_hash=store_hash).first()
154 | if store is None:
155 | store = Store(store_hash, access_token, scope)
156 | db.session.add(store)
157 | db.session.commit()
158 | else:
159 | store.access_token = access_token
160 | store.scope = scope
161 | db.session.add(store)
162 | db.session.commit()
163 | # If the app was installed before, make sure the old admin user is no longer marked as the admin
164 | oldadminuser = StoreUser.query.filter_by(store_id=store.id, admin=True).first()
165 | if oldadminuser:
166 | oldadminuser.admin = False
167 | db.session.add(oldadminuser)
168 |
169 | # Create or update global BC user
170 | user = User.query.filter_by(bc_id=bc_user_id).first()
171 | if user is None:
172 | user = User(bc_user_id, email)
173 | db.session.add(user)
174 | elif user.email != email:
175 | user.email = email
176 | db.session.add(user)
177 |
178 | # Create or update store user
179 | storeuser = StoreUser.query.filter_by(user_id=user.id, store_id=store.id).first()
180 | if not storeuser:
181 | storeuser = StoreUser(store, user, admin=True)
182 | else:
183 | storeuser.admin = True
184 | db.session.add(storeuser)
185 | db.session.commit()
186 |
187 | # Log user in and redirect to app home
188 | flask.session['storeuserid'] = storeuser.id
189 | return flask.redirect(app.config['APP_URL'])
190 |
191 |
192 | # The Load URL. See https://developer.bigcommerce.com/api/load
193 | @app.route('/bigcommerce/load')
194 | def load():
195 | # Decode and verify payload
196 | payload = flask.request.args['signed_payload_jwt']
197 | try:
198 | user_data = BigcommerceApi.oauth_verify_payload_jwt(payload, client_secret(), client_id())
199 | except Exception as e:
200 | return jwt_error(e)
201 |
202 | bc_user_id = user_data['user']['id']
203 | email = user_data['user']['email']
204 | store_hash = user_data['sub'].split('stores/')[1]
205 |
206 | # Lookup store
207 | store = Store.query.filter_by(store_hash=store_hash).first()
208 | if store is None:
209 | return "Store not found!", 401
210 |
211 | # Lookup user and create if doesn't exist (this can happen if you enable multi-user
212 | # when registering your app)
213 | user = User.query.filter_by(bc_id=bc_user_id).first()
214 | if user is None:
215 | user = User(bc_user_id, email)
216 | db.session.add(user)
217 | db.session.commit()
218 | storeuser = StoreUser.query.filter_by(user_id=user.id, store_id=store.id).first()
219 | if storeuser is None:
220 | storeuser = StoreUser(store, user)
221 | db.session.add(storeuser)
222 | db.session.commit()
223 |
224 | # Log user in and redirect to app interface
225 | flask.session['storeuserid'] = storeuser.id
226 | return flask.redirect(app.config['APP_URL'])
227 |
228 |
229 | # The Uninstall URL. See https://developer.bigcommerce.com/api/load
230 | @app.route('/bigcommerce/uninstall')
231 | def uninstall():
232 | # Decode and verify payload
233 | payload = flask.request.args['signed_payload_jwt']
234 | try:
235 | user_data = BigcommerceApi.oauth_verify_payload_jwt(payload, client_secret(), client_id())
236 | except Exception as e:
237 | return jwt_error(e)
238 |
239 | # Lookup store
240 | store_hash = user_data['sub'].split('stores/')[1]
241 | store = Store.query.filter_by(store_hash=store_hash).first()
242 | if store is None:
243 | return "Store not found!", 401
244 |
245 | # Clean up: delete store associated users. This logic is up to you.
246 | # You may decide to keep these records around in case the user installs
247 | # your app again.
248 | storeusers = StoreUser.query.filter_by(store_id=store.id)
249 | for storeuser in storeusers:
250 | db.session.delete(storeuser)
251 | db.session.delete(store)
252 | db.session.commit()
253 |
254 | return flask.Response('Deleted', status=204)
255 |
256 |
257 | # The Remove User Callback URL.
258 | @app.route('/bigcommerce/remove-user')
259 | def remove_user():
260 | payload = flask.request.args['signed_payload_jwt']
261 | try:
262 | user_data = BigcommerceApi.oauth_verify_payload_jwt(payload, client_secret(), client_id())
263 | except Exception as e:
264 | return jwt_error(e)
265 |
266 | store_hash = user_data['sub'].split('stores/')[1]
267 | store = Store.query.filter_by(store_hash=store_hash).first()
268 | if store is None:
269 | return "Store not found!", 401
270 |
271 | # Lookup user and delete it
272 | bc_user_id = user_data['user']['id']
273 | user = User.query.filter_by(bc_id=bc_user_id).first()
274 | if user is not None:
275 | storeuser = StoreUser.query.filter_by(user_id=user.id, store_id=store.id).first()
276 | db.session.delete(storeuser)
277 | db.session.commit()
278 |
279 | return flask.Response('Deleted', status=204)
280 |
281 |
282 | #
283 | # App interface
284 | #
285 | @app.route('/')
286 | def index():
287 | # Lookup user
288 | storeuser = StoreUser.query.filter_by(id=flask.session['storeuserid']).first()
289 | if storeuser is None:
290 | return "Not logged in!", 401
291 | store = storeuser.store
292 | user = storeuser.user
293 |
294 | # Construct api client
295 | client = BigcommerceApi(client_id=client_id(),
296 | store_hash=store.store_hash,
297 | access_token=store.access_token)
298 |
299 | # Fetch a few products
300 | products = client.Products.all(limit=10)
301 |
302 | # Render page
303 | context = dict()
304 | context['products'] = products
305 | context['user'] = user
306 | context['store'] = store
307 | context['client_id'] = client_id()
308 | context['api_url'] = client.connection.host
309 | return render('index.html', context)
310 |
311 |
312 | @app.route('/instructions')
313 | def instructions():
314 | if not app.config['DEBUG']:
315 | return "Forbidden - instructions only visible in debug mode"
316 | context = dict()
317 | return render('instructions.html', context)
318 |
319 |
320 | if __name__ == "__main__":
321 | db.create_all()
322 | app.run(app.config['LISTEN_HOST'], app.config['LISTEN_PORT'])
323 |
--------------------------------------------------------------------------------