Security review fixes: admin permissions, guest sessions, payment edge cases, assistant leaks
- Admin: refund/cancel/mark-resolved/Bazaar-link actions need change permission; question and curriculum import need add/delete; content browser needs view_question; guest device ids are shown to superusers only. - Accounts: registering a guest's phone revokes the guest's earlier sessions; self-deletion keeps Redemption, UserOffer and the phone's OTP rows; the public demo guest cannot be deleted (DEMO_GUEST_DEVICE_IDS); display-name contact filter covers any separator and social handles. - Payment: callback without Authority is 400; a second Bazaar purchase on a paid order is 409; orders snapshot credits/hearts/days at creation (payment.0011). - Assistant: a broken diagram no longer makes a streamed answer free; a late off-topic marker refuses the answer (not stored, not cached); a disconnect saves the partial answer; only KNOWN_COLLECTIONS are searched. - Infra: secret-key file is created atomically (workers no longer split keys); no default INTERNAL_SERVICE_KEY; Kavenegar key and Bazaar token kept out of error logs; Swagger/ReDoc pinned; schema throttled; docker compose bound to 127.0.0.1; non-ASCII digit ids are 400; ticket text capped at 5000. - docs/security-before-production.md: remaining open items 24-31 and the guard tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -100,12 +100,18 @@ def create_order(*, user_id: int, amount: int, currency: str = 'IRT',
|
||||
payment_url = None if existing.provider == 'bazaar' else call_payment_gateway(existing)
|
||||
return existing, payment_url
|
||||
|
||||
from subscription.models import Plan
|
||||
|
||||
plan = Plan.objects.filter(pk=plan_id).first() if plan_id else None
|
||||
try:
|
||||
with db_transaction.atomic():
|
||||
order = Order.objects.create(
|
||||
user_id=user_id,
|
||||
subscription_id=subscription_id,
|
||||
plan_id=plan_id,
|
||||
question_credits=plan.question_credits if plan else None,
|
||||
hearts_days=plan.hearts_days if plan else None,
|
||||
duration_days=plan.duration_days if plan else None,
|
||||
amount=amount,
|
||||
list_price=amount,
|
||||
plan_title=plan_title[:50],
|
||||
@@ -206,7 +212,8 @@ def apply_order_payment_result(*, order: Order, gateway: str, gateway_ref_id: st
|
||||
if newly_paid:
|
||||
if order.subscription_id:
|
||||
from subscription.services import activate_subscription
|
||||
activate_subscription(order.subscription_id, plan_id=order.plan_id)
|
||||
activate_subscription(order.subscription_id, plan_id=order.plan_id,
|
||||
duration_days=order.duration_days)
|
||||
elif order.plan_id:
|
||||
# بستهی سوال: اعتبار و روزهای قلبِ نامحدود، بیدست زدن به اشتراک.
|
||||
from subscription.services import grant_pack
|
||||
@@ -314,7 +321,7 @@ def refund_order(order: Order, *, staff: str = '') -> dict:
|
||||
|
||||
if order.subscription_id:
|
||||
sub = Subscription.objects.select_for_update().filter(pk=order.subscription_id).first()
|
||||
days = plan.duration_days if plan else 30
|
||||
days = order.duration_days if order.duration_days is not None else (plan.duration_days if plan else 30)
|
||||
if sub is not None and sub.expires_at is not None:
|
||||
sub.expires_at = max(sub.expires_at - timezone.timedelta(days=days), now)
|
||||
if sub.expires_at <= now:
|
||||
@@ -331,12 +338,13 @@ def refund_order(order: Order, *, staff: str = '') -> dict:
|
||||
if take:
|
||||
QuestionCredit.objects.create(user_id=order.user_id, delta=-take, reason='refund',
|
||||
order_id=order.id, note=f'بازپرداختِ سفارشِ {order.id}')
|
||||
if plan is not None and plan.hearts_days:
|
||||
hearts_days = order.hearts_days if order.hearts_days is not None else (plan.hearts_days if plan else 0)
|
||||
if hearts_days:
|
||||
hp = HeartsPass.objects.select_for_update().filter(user_id=order.user_id).first()
|
||||
if hp is not None:
|
||||
hp.expires_at = max(hp.expires_at - timezone.timedelta(days=plan.hearts_days), now)
|
||||
hp.expires_at = max(hp.expires_at - timezone.timedelta(days=hearts_days), now)
|
||||
hp.save(update_fields=['expires_at'])
|
||||
summary['hearts_days'] = plan.hearts_days
|
||||
summary['hearts_days'] = hearts_days
|
||||
|
||||
from affiliate.models import Conversion
|
||||
Conversion.objects.filter(order_id=order.id, status='paid').update(
|
||||
|
||||
Reference in New Issue
Block a user