let the Bazaar verify URL double as the OAuth redirect URI

The Bazaar account owner registered /api/payment/bazaar/verify as the
redirect URI. POST stays the app's purchase check; GET now takes Bazaar's
?code=, exchanges it for a refresh token and stores it in the new
BazaarCredential row, so connecting needs no SSH or .env edit.

The GET is unauthenticated, so it only acts inside a window opened from
the admin (or bazaar_authorize_link): 72 h by default, closed by the first
successful exchange, state checked when Bazaar echoes it. Authorize/token
URL shapes follow the known devapi v2 flow; not checked against Bazaar's
current docs, which did not load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 21:30:44 +03:30
parent 5d490edaf9
commit 8763ffd7bf
9 changed files with 414 additions and 9 deletions

View File

@@ -2,7 +2,8 @@ from urllib.parse import urlsplit
from django.conf import settings
from django.db import transaction
from django.http import HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.html import escape
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
@@ -219,9 +220,24 @@ class ZarinpalCallbackView(APIView):
return Response(ZarinpalCallbackResponseSerializer(data).data)
def _bazaar_oauth_page(message: str, *, status: int) -> HttpResponse:
# این را صاحبِ حسابِ بازار در مرورگر می‌بیند، نه اپ؛ پس HTML ساده.
return HttpResponse(
'<!doctype html><meta charset="utf-8"><title>اتصال به بازار</title>'
f'<p dir="rtl" style="font:16px sans-serif;margin:2em">{escape(message)}</p>',
status=status,
)
class BazaarVerifyPurchaseView(APIView):
"""
POST /api/payment/bazaar/verify — تایید پرداخت درون‌برنامه‌ای بازار
GET /api/payment/bazaar/verify?code=… — Redirect URIِ OAuthِ بازار
همین آدرس در پنلِ بازار Redirect URI ثبت شده (خواستِ مدیرِ حساب،
۲۰۲۶-۰۹-۲۶)، پس دو کار دارد که با متد از هم جدا می‌شوند: GET را مرورگرِ
صاحبِ حساب بعد از «اجازه» می‌زند و بی‌احراز است (فقط داخلِ پنجره‌ای که
start_authorization باز کرده اثر دارد)؛ POST را اپِ اندروید بعد از خرید.
برخلاف درگاه معمولی که با webhook خودِ درگاه تایید می‌شه، در پرداخت
درون‌برنامه‌ای بازار هیچ webhookی نیست: کلاینت اندروید بعد از خرید از
@@ -241,6 +257,33 @@ class BazaarVerifyPurchaseView(APIView):
throttle_scope = 'bazaar_verify'
throttle_classes = [GuestAwareBazaarThrottle]
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return super().get_permissions()
def get_throttles(self):
if self.request.method == 'GET':
self.throttle_scope = 'bazaar_oauth'
return super().get_throttles()
@extend_schema(exclude=True)
def get(self, request):
if request.query_params.get('error'):
return _bazaar_oauth_page(
f'بازار اجازه نداد: {request.query_params.get("error")}', status=400,
)
code = request.query_params.get('code', '')
if not code:
return _bazaar_oauth_page(
'این آدرس برای برگشت از بازار است و بدون code کاری نمی‌کند.', status=400,
)
try:
bazaar.complete_authorization(code=code, state=request.query_params.get('state', ''))
except bazaar.BazaarAuthorizationError as exc:
return _bazaar_oauth_page(str(exc), status=400)
return _bazaar_oauth_page('اتصال به بازار انجام شد. می‌توانید این صفحه را ببندید.', status=200)
@extend_schema(
request=BazaarVerifyRequestSerializer,
responses=BazaarVerifyResponseSerializer,