diff --git a/application/backend/app/api/__init__.py b/application/backend/app/api/__init__.py index 2dd97e6a..48e72957 100644 --- a/application/backend/app/api/__init__.py +++ b/application/backend/app/api/__init__.py @@ -14,6 +14,7 @@ from app.api.crud.groups import bp as groups_bp from app.api.auth import bp as auth_bp from app.api.slack import bp as slack_bp +from app.api.stripe import bp as stripe_bp from flask_smorest import Api from flask_marshmallow import Marshmallow diff --git a/application/backend/app/api/stripe.py b/application/backend/app/api/stripe.py new file mode 100644 index 00000000..d659e46e --- /dev/null +++ b/application/backend/app/api/stripe.py @@ -0,0 +1,94 @@ +import requests +import json +import logging +from flask import views, request, redirect, jsonify, current_app +from flask_smorest import Blueprint, abort +from app.repositories.user_repository import UserRepository +from app.models.user_schema import UserSchema +from app.auth import auth +from flask_jwt_extended import create_access_token, create_refresh_token, jwt_required, get_jwt_identity, set_access_cookies, unset_jwt_cookies +from app.services.slack_organization_service import SlackOrganizationService +from app.services.injector import injector +import stripe + +bp = Blueprint("stripe", "stripe", url_prefix="/stripe", description="Stripe payments") + +@bp.route("/products") +class Stripe(views.MethodView): + #@jwt_required() + def get(self): + # identity = get_jwt_identity() + # user = UserRepository.get_by_id(identity) + products = stripe.Price.list(expand=['data.product']) + return jsonify(products) + + +@bp.route('/create-checkout-session') +class Stripe(views.MethodView): + def post(self): + try: + prices = stripe.Price.list( + expand=['data.product'] + ) + # print(prices) + + checkout_session = stripe.checkout.Session.create( + line_items=[ + { + 'price': prices.data[0].id, + 'quantity': 1, + }, + ], + mode='subscription', + success_url="https://google.com", + cancel_url="https://yahoo.com", + ) + res = jsonify({'sessionId': checkout_session['id'], 'url': checkout_session['url']}) + + return res + except Exception as e: + return jsonify(e) + + +@bp.route('/webhook') +class Stripe(views.MethodView): + def post(self): + logger = injector.get(logging.Logger) + payload = request.data + event = None + + # TODO: this is not secure, enforce secret for production + endpoint_secret = None #app.config['STRIPE_WEBHOOK_SECRET'] + + try: + # Only verify the event if there is an endpoint secret defined + # Otherwise use the basic event deserialized with json + if endpoint_secret: + sig_header = request.headers['STRIPE_SIGNATURE'] + + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + else: + event = json.loads(payload) + + # Handle the event + if event['type'] == 'payment_intent.succeeded': + payment_intent = event['data']['object'] + print(payment_intent) + + # ... handle other event types + else: + print('Unhandled event type {}'.format(event['type'])) + + return jsonify(success=True) + + except ValueError as e: + # Invalid payload + logger.error('Failed to parse stripe webhook. ' + str(e)) + return jsonify(success=False) + except stripe.error.SignatureVerificationError as e: + # Invalid signature + logger.warn('⚠️ Stripe webhook signature verification failed.' + str(e)) + return jsonify(success=False) + diff --git a/application/backend/app/application.py b/application/backend/app/application.py index e2330480..dda5a0bd 100644 --- a/application/backend/app/application.py +++ b/application/backend/app/application.py @@ -3,6 +3,7 @@ import logging import sys import cloudinary +import stripe from app.db import db, migrate from app.api import api, ma @@ -20,7 +21,7 @@ from flask_talisman import Talisman from flask_cors import CORS -from app.api import events_bp, restaurants_bp, users_bp, invitations_bp, images_bp, auth_bp, slack_bp, groups_bp +from app.api import events_bp, restaurants_bp, users_bp, invitations_bp, images_bp, auth_bp, slack_bp, groups_bp, stripe_bp # Don't remove, needed for queue files to get imported import app.services.broker.queue @@ -139,6 +140,9 @@ def add_headers(response): secure=True, ) + # Set up stripe + stripe.api_key = app.config["STRIPE_SECRET_KEY"] + return app def register_blueprints(api): @@ -152,4 +156,5 @@ def register_blueprints(api): api_base_bp.register_blueprint(auth_bp) api_base_bp.register_blueprint(slack_bp) api_base_bp.register_blueprint(groups_bp) + api_base_bp.register_blueprint(stripe_bp) api.register_blueprint(api_base_bp) diff --git a/application/backend/app/config.py b/application/backend/app/config.py index 1c376a7a..d62ee9aa 100644 --- a/application/backend/app/config.py +++ b/application/backend/app/config.py @@ -22,6 +22,8 @@ class Base(object): JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=30) JWT_TOKEN_LOCATION = ["headers", "cookies"] JWT_COOKIE_SECURE = True + # Stripe secret key + STRIPE_SECRET_KEY = os.environ.get("STRIPE_SECRET_KEY") class Test(Base): diff --git a/application/backend/requirements.txt b/application/backend/requirements.txt index b19f1b6a..eb2df8c5 100644 --- a/application/backend/requirements.txt +++ b/application/backend/requirements.txt @@ -18,6 +18,7 @@ flask_cors==3.0.10 sqlalchemy_utils==0.38.3 rabbitmq-pika-flask==1.2.31 jsonschema==4.17.3 +stripe==6.7 pytz injector cloudinary diff --git a/application/containers/development/.env.example b/application/containers/development/.env.example index 3b50cd34..f4d24a58 100644 --- a/application/containers/development/.env.example +++ b/application/containers/development/.env.example @@ -14,3 +14,6 @@ CLOUDINARY_API_SECRET= # URI's - Dont change unless host and ports are changed FRONTEND_URI=https://localhost:4000 # exposed port routed from nginx BACKEND_URI=http://backend:3000 # docker-defined host w/ port from flask + +# Stripe variables +STRIPE_SECRET_KEY= diff --git a/application/containers/development/Dockerfile.stripe-cli b/application/containers/development/Dockerfile.stripe-cli new file mode 100644 index 00000000..7fdc7df0 --- /dev/null +++ b/application/containers/development/Dockerfile.stripe-cli @@ -0,0 +1,5 @@ +FROM stripe/stripe-cli:latest + +WORKDIR /srv/stripe-cli + + diff --git a/application/containers/development/docker-compose.yml b/application/containers/development/docker-compose.yml index a5ff561c..954466d9 100644 --- a/application/containers/development/docker-compose.yml +++ b/application/containers/development/docker-compose.yml @@ -66,7 +66,7 @@ services: rabbitmq: condition: service_healthy ports: - - 8080:8080 + - 8080:3000 volumes: - ../../backend:/srv/backend environment: @@ -89,10 +89,24 @@ services: *common-rabbitmq-variables, *common-cloudinary-variables, ] + STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY}" networks: - database-network - proxy-network - rabbitmq_network + - stripe-cli-network + stripe-cli: + build: + context: ../../ + dockerfile: containers/development/Dockerfile.stripe-cli + depends_on: + - backend + environment: + STRIPE_SECRET_KEY: "${STRIPE_SECRET_KEY}" + WEBHOOK_ENDPOINT: "${BACKEND_URI}/api/stripe/webhook" + command: "listen --api-key \"${STRIPE_API_KEY}\" --forward-to \"${BACKEND_URI}/api/stripe/webhook\"" + networks: + - stripe-cli-network bot-worker: build: context: ../../ @@ -168,6 +182,8 @@ networks: driver: bridge rabbitmq_network: driver: bridge + stripe-cli-network: + driver: bridge volumes: database_data: