From e382397c9e7f896f64cc02cedc0e4b500bc8978f Mon Sep 17 00:00:00 2001 From: vincenzoarcidiacono Date: Mon, 12 Feb 2024 17:21:44 +0100 Subject: [PATCH] feat(form): Add stripe component. --- schedula/utils/form/__init__.py | 5 +- schedula/utils/form/config.py | 11 ++ schedula/utils/form/react/deploy.sh | 2 +- .../form/react/src/core/components/index.js | 2 + schedula/utils/form/server.py | 114 ++++++++++++++++-- .../utils/form/templates/schedula/base.html | 3 +- 6 files changed, 122 insertions(+), 15 deletions(-) diff --git a/schedula/utils/form/__init__.py b/schedula/utils/form/__init__.py index 96ba39881..f0ed2cf56 100644 --- a/schedula/utils/form/__init__.py +++ b/schedula/utils/form/__init__.py @@ -312,8 +312,9 @@ def add2csrf_protected(self, app=None, item=None): else: if app.secret_key is None: app.secret_key = secrets.token_hex(32) - for endpoint in app.view_functions: - self._csrf_protected.add(('view', endpoint)) + for endpoint, func in app.view_functions.items(): + if not getattr(func, 'csrf_exempt', False): + self._csrf_protected.add(('view', endpoint)) return app def app(self, root_path=None, depth=1, mute=False, blueprint_name=None, diff --git a/schedula/utils/form/config.py b/schedula/utils/form/config.py index 1df2f7fc4..1107f3eb0 100644 --- a/schedula/utils/form/config.py +++ b/schedula/utils/form/config.py @@ -129,3 +129,14 @@ class Config: # many DBaaS options automatically close idle connections. SQLALCHEMY_ENGINE_OPTIONS = {"pool_pre_ping": True} SQLALCHEMY_TRACK_MODIFICATIONS = False + + STRIPE_SECRET_KEY = os.environ.get( + "STRIPE_SECRET_KEY", 'sk_test_EP9uAZ5ZF1yz1LYTXlCWbRh0' + ) + STRIPE_PUBLISHABLE_KEY = os.environ.get( + "STRIPE_PUBLISHABLE_KEY", 'pk_test_5IqeFypqBcFlrqZ7KWGWA21H' + ) + STRIPE_WEBHOOK_SECRET_KEY = os.environ.get( + "STRIPE_WEBHOOK_SECRET_KEY", + 'whsec_a611c4ad09e38ff44444e13635ba01f6a62acbb2f566fb1c945560e9e46e3556' + ) diff --git a/schedula/utils/form/react/deploy.sh b/schedula/utils/form/react/deploy.sh index d94b2c141..c926a7c7a 100755 --- a/schedula/utils/form/react/deploy.sh +++ b/schedula/utils/form/react/deploy.sh @@ -8,7 +8,7 @@ rm -r ../static/schedula/css mkdir "../static/schedula/css" rm -r ../static/schedula/media mkdir "../static/schedula/media" - +sudo n stable echo "Installing dependencies..." npm i --force echo "Bundle index..." diff --git a/schedula/utils/form/react/src/core/components/index.js b/schedula/utils/form/react/src/core/components/index.js index e8bac94b3..80a056de0 100644 --- a/schedula/utils/form/react/src/core/components/index.js +++ b/schedula/utils/form/react/src/core/components/index.js @@ -7,6 +7,7 @@ const Domain = React.lazy(() => import('./Domain')); const Element = React.lazy(() => import('./Element')); const FlexLayout = React.lazy(() => import('./FlexLayout')); const Static = React.lazy(() => import('./Static')); +const Stripe = React.lazy(() => import('./Stripe')); const Title = React.lazy(() => import('./Title')); @@ -18,6 +19,7 @@ export function generateComponents() { Element, FlexLayout, Static, + Stripe, Title } } diff --git a/schedula/utils/form/server.py b/schedula/utils/form/server.py index 8fbc6e567..6b1255a8b 100644 --- a/schedula/utils/form/server.py +++ b/schedula/utils/form/server.py @@ -9,8 +9,10 @@ """ It provides functions to build the base form flask app. """ +import logging import secrets import datetime +import collections import os.path as osp import schedula as sh from .mail import Mail @@ -33,11 +35,14 @@ Security, SQLAlchemyUserDatastore, current_user as cu, auth_required ) +log = logging.getLogger(__name__) + def default_get_form_context(): return { 'userInfo': getattr(cu, "get_security_payload", lambda: {})(), - 'reCAPTCHA': current_app.config.get('RECAPTCHA_PUBLIC_KEY') + 'reCAPTCHA': current_app.config.get('RECAPTCHA_PUBLIC_KEY'), + 'stripeKey': current_app.config.get('STRIPE_PUBLISHABLE_KEY') } @@ -46,15 +51,6 @@ def basic_app(sitemap, app): if getattr(sitemap, 'basic_app_config'): app.config.from_object(sitemap.basic_app_config) - @app.after_request - def add_security_headers(resp): - from flask import session - nonce = session.get('nonce') - if not nonce: - session['nonce'] = nonce = secrets.token_urlsafe(16) - resp.headers['Content-Security-Policy'] = f"script-src 'nonce-{nonce}'" - return resp - # Create database connection object db = SQLAlchemy(app) @@ -165,6 +161,7 @@ class User(db.Model, fsqla.FsUserMixin): def get_security_payload(self): return {k: v for k, v in { + 'id': self.id, 'email': self.email, 'username': self.username, 'firstname': self.firstname, @@ -264,4 +261,101 @@ def get_locale(): Babel(app, locale_selector=get_locale) mail = Mail(app) + + @app.route('/stripe/create-checkout-session', methods=['POST']) + def create_payment(): + import stripe + try: + data = request.get_json() if request.is_json else dict(request.form) + if not isinstance(data, list): + data = data, + lookup_keys = collections.OrderedDict() + api_key = current_app.config.get('STRIPE_SECRET_KEY') + for i, d in enumerate(data): + if 'lookup_key' in d: + sh.get_nested_dicts( + lookup_keys, d['lookup_key'], default=list + ).append(i) + if lookup_keys: + for price, it in zip(stripe.Price.list( + api_key=api_key, + lookup_keys=list(lookup_keys.keys()), + expand=['data.product'] + ).data, lookup_keys.values()): + for i in it: + data[i].update({'price': price.id}) + session = stripe.checkout.Session.create( + api_key=api_key, + ui_mode='embedded', + line_items=data, + mode='payment', + automatic_tax={'enabled': True}, + redirect_on_completion='never', + metadata={ + # 'customer': f'{cu.id} - {cu.firstname} {cu.lastname}' + } + ) + except Exception as e: + return jsonify(error=str(e)) + + return jsonify( + clientSecret=session.client_secret, sessionId=session.id + ) + + @app.route('/stripe/session-status', methods=['GET']) + def session_status(): + import stripe + session = stripe.checkout.Session.retrieve( + request.args.get('session_id'), + api_key=current_app.config.get('STRIPE_SECRET_KEY') + ) + session.customer_details.email + status = session.status + if status == "complete": + msg = 'Payment succeeded!' + category = 'success' + elif status == "processing": + msg = 'Your payment is processing.' + category = 'success' + elif status == "requires_payment_method": + msg = 'Your payment was not successful, please try again.' + category = 'success' + else: + msg = 'Something went wrong.' + category = 'success' + flash(str(lazy_gettext(msg)), category) + return jsonify( + status=status, + customer_email=session.customer_details.email, + userInfo=getattr(cu, "get_security_payload", lambda: {})() + ) + + @app.route('/stripe/webhook', methods=['POST']) + def stripe_webhook(): + import stripe + payload = request.data + sig_header = request.headers['STRIPE_SIGNATURE'] + + try: + event = stripe.Webhook.construct_event( + payload, sig_header, + current_app.config.get('STRIPE_WEBHOOK_SECRET_KEY') + ) + except ValueError as e: + # Invalid payload + raise e + except stripe.error.SignatureVerificationError as e: + # Invalid signature + raise e + + # Handle the event + if event['type'] == 'payment_intent.succeeded': + payment_intent = event['data']['object'] + # ... handle other event types + else: + log.info('Unhandled event type {}'.format(event['type'])) + + return jsonify(success=True) + + stripe_webhook.csrf_exempt = True return app diff --git a/schedula/utils/form/templates/schedula/base.html b/schedula/utils/form/templates/schedula/base.html index 4c40cebd8..789895bcd 100644 --- a/schedula/utils/form/templates/schedula/base.html +++ b/schedula/utils/form/templates/schedula/base.html @@ -128,8 +128,7 @@ formData, editOnChange, preSubmit, - postSubmit, - nonce: "{{ session['nonce'] | tojson | safe }}" + postSubmit }) }); }