diff --git a/donation-api/Dockerfile b/donation-api/Dockerfile index 33cb4ba9..a77d87e2 100644 --- a/donation-api/Dockerfile +++ b/donation-api/Dockerfile @@ -9,10 +9,12 @@ COPY pyproject.toml README.md /src/ COPY src/donation_api/__about__.py /src/src/donation_api/__about__.py # Install Python dependencies -RUN pip install --no-cache-dir /src +RUN apk --no-cache add dumb-init \ + && pip install --no-cache-dir /src COPY src /src/src COPY *.md /src/ +COPY entrypoint.sh /usr/local/bin/entrypoint # Install + cleanup RUN pip install --no-cache-dir /src \ @@ -21,8 +23,25 @@ RUN pip install --no-cache-dir /src \ # set STRIPE_USE_LIVE=1 for production (use of live key) ENV STRIPE_USE_LIVE=0 -ENV STRIPE_TEST_KEY=notset -ENV STRIPE_LIVE_KEY=notset +ENV STRIPE_TEST_PUBLISHABLE_KEY=notset +ENV STRIPE_TEST_SECRET_KEY=notset +ENV STRIPE_LIVE_PUBLISHABLE_KEY=notset +ENV STRIPE_LIVE_SECRET_KEY=notset ENV STRIPE_WEBHOOK_SECRET="" +ENV STRIPE_MINIMAL_AMOUNT=5 +ENV STRIPE_MAXIMUM_AMOUNT=999999 +ENV STRIPE_WEBHOOK_TESTING_IPS= +ENV ALLOWED_CURRENCIES=chf|usd|eur +ENV MERCHANTID_DOMAIN_ASSOCIATION= +ENV MERCHANTID_DOMAIN_ASSOCIATION_TXT= +ENV APPLEPAY_MERCHANT_IDENTIFIER= +ENV APPLEPAY_DISPLAYNAME= +ENV APPLEPAY_PAYMENT_SESSION_INITIATIVE= +ENV APPLEPAY_PAYMENT_SESSION_INITIATIVE_CONTEXT= +# ENV APPLEPAY_PAYMENT_SESSION_REQ_TIMEOUT_SEC=5 +ENV APPLEPAY_MERCHANT_CERTIFICATE_PATH=/etc/ssl/certs/applepay_merchant.pem +ENV APPLEPAY_MERCHANT_CERTIFICATE_KEY_PATH=/etc/ssl/certs/applepay_merchant.key + +ENTRYPOINT ["/usr/bin/dumb-init", "--", "/usr/local/bin/entrypoint"] CMD ["uvicorn", "donation_api.entrypoint:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/donation-api/pyproject.toml b/donation-api/pyproject.toml index 917577fd..f7dbce59 100644 --- a/donation-api/pyproject.toml +++ b/donation-api/pyproject.toml @@ -9,7 +9,8 @@ description = "A simple Stripe relay endpoint" readme = "README.md" dependencies = [ "stripe==11.2.0", - "fastapi[standard]==0.115.5" + "fastapi[standard]==0.115.5", + "requests==2.32.3", ] dynamic = ["authors", "classifiers", "keywords", "license", "version", "urls"] diff --git a/donation-api/src/donation_api/constants.py b/donation-api/src/donation_api/constants.py index fc96d6b2..22515146 100644 --- a/donation-api/src/donation_api/constants.py +++ b/donation-api/src/donation_api/constants.py @@ -1,4 +1,5 @@ import os +import pathlib from dataclasses import dataclass, field import requests @@ -26,6 +27,20 @@ class Constants: os.getenv("MERCHANTID_DOMAIN_ASSOCIATION_TXT") or "" ) + applepay_merchant_identifier: str = os.getenv("APPLEPAY_MERCHANT_IDENTIFIER") or "" + applepay_displayname: str = os.getenv("APPLEPAY_DISPLAYNAME") or "" + applepay_payment_session_initiative: str = ( + os.getenv("APPLEPAY_PAYMENT_SESSION_INITIATIVE") or "" + ) + applepay_payment_session_initiative_context: str = ( + os.getenv("APPLEPAY_PAYMENT_SESSION_INITIATIVE_CONTEXT") or "" + ) + applepay_merchant_certificate_path: pathlib.Path = pathlib.Path("/missing") + applepay_merchant_certificate_key_path: pathlib.Path = pathlib.Path("/missing") + applepay_payment_session_request_timeout: int = int( + os.getenv("APPLEPAY_PAYMENT_SESSION_REQ_TIMEOUT_SEC") or "5" + ) + stripe_minimal_amount: int = int(os.getenv("STRIPE_MINIMAL_AMOUNT") or "5") stripe_maximum_amount: int = int(os.getenv("STRIPE_MAXIMUM_AMOUNT") or "999999") @@ -44,6 +59,25 @@ def __post_init__(self): if not self.stripe_webhook_sender_ips: raise OSError("No Stripe Webhook IPs!") + if ( + self.applepay_payment_session_initiative + and self.applepay_payment_session_initiative not in ("web", "in_app") + ): + raise OSError("ApplePay Payment Session Initiative in invalid") + + if ( + self.applepay_payment_session_initiative + and not self.applepay_payment_session_initiative_context + ): + raise OSError("Missing ApplePay Payment Initiative Context") + + certpath = os.getenv("APPLEPAY_MERCHANT_CERTIFICATE_PATH") or "" + if certpath: + self.applepay_merchant_certificate_path = pathlib.Path(certpath) + certkeypath = os.getenv("APPLEPAY_MERCHANT_CERTIFICATE_KEY_PATH") or "" + if certkeypath: + self.applepay_merchant_certificate_key_path = pathlib.Path(certpath) + @property def stripe_secret_api_key(self) -> str: if self.stripe_on_prod: diff --git a/donation-api/src/donation_api/stripe.py b/donation-api/src/donation_api/stripe.py index 1569d509..d7be7584 100644 --- a/donation-api/src/donation_api/stripe.py +++ b/donation-api/src/donation_api/stripe.py @@ -3,6 +3,7 @@ from http import HTTPStatus from typing import Annotated, Any +import requests import stripe from fastapi import APIRouter, Depends, Header, HTTPException, Request from pydantic import BaseModel, ConfigDict @@ -60,6 +61,10 @@ class StripeWebhookResponse(BaseModel): status: str +class OpaqueApplePayPaymentSession(BaseModel): + model_config = ConfigDict(extra="allow") + + async def get_body(request: Request): """raw request body""" return await request.body() @@ -128,6 +133,24 @@ async def check_config(): if not conf.alllowed_currencies: errors.append("Missing currencies list") + if not conf.applepay_merchant_identifier: + errors.append("Missing ApplePay merchantIdentifier") + + if not conf.applepay_displayname: + errors.append("Missing ApplePay displayName") + + if not conf.applepay_payment_session_initiative: + errors.append("Missing ApplePay session initiative") + + if not conf.applepay_payment_session_initiative_context: + errors.append("Missing ApplePay session initiative context") + + if not conf.applepay_merchant_certificate_path.read_text(): + errors.append("Missing ApplePay merchant certificate") + + if not conf.applepay_merchant_certificate_key_path.read_text(): + errors.append("Missing ApplePay merchant certificate key") + if errors: raise HTTPException( status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="\n".join(errors) @@ -247,3 +270,47 @@ def webhook_received( logger.info("❌ Payment failed.") return {"status": "success"} + + +@router.post( + "/payment-session", + responses={ + HTTPStatus.BAD_REQUEST: { + "description": "Request for a Payment Session from ApplePay failed", + }, + HTTPStatus.OK: { + "model": OpaqueApplePayPaymentSession, + "description": "ApplePay Server returned an Opaque Payment Session", + }, + }, + status_code=HTTPStatus.OK, +) +async def create_payment_session(): + payload = { + "merchantIdentifier": conf.applepay_merchant_identifier, + "displayName": conf.applepay_displayname, + "initiative": conf.applepay_payment_session_initiative, + "initiativeContext": conf.applepay_payment_session_initiative_context, + } + + data: dict[str, Any] = {} + resp = requests.post( + url="https://apple-pay-gateway.apple.com/paymentservices/paymentSession", + cert=( + str(conf.applepay_merchant_certificate_path), + str(conf.applepay_merchant_certificate_key_path), + ), + json=payload, + timeout=conf.applepay_payment_session_request_timeout, + ) + try: + data = resp.json() + except Exception: + ... + if resp.status_code != HTTPStatus.OK: + raise HTTPException( + status_code=resp.status_code, + detail=data.get("statusMessage") or "Failed to request payment session", + ) + + return data