Fix #3: Remove client-exploitable payment endpoint

- Payment endpoint no longer uses @_require_auth (not client-callable)
- Identifies user from webhook payload user_id instead of client JWT
- Removed hardcoded payment secret from chat.js
- Client now shows placeholder message directing to admin
- Webhook secret + user_id must come from payment provider server
This commit is contained in:
3nd3r 2026-04-12 12:51:31 -05:00
parent 8da91ebf70
commit be3503b31b
2 changed files with 16 additions and 28 deletions

View File

@ -291,12 +291,12 @@ def ai_message():
# ---------------------------------------------------------------------------
@api.route("/payment/success", methods=["POST"])
@_require_auth
def payment_success():
"""
Validate a payment webhook and flip user.has_ai_access.
Server-side payment webhook NOT callable by clients.
Expected body: { "secret": "<PAYMENT_SECRET>" }
Validates the webhook secret and unlocks AI access for the user
identified by the 'user_id' field in the JSON body.
For Stripe production: replace the secret comparison with
stripe.Webhook.construct_event() using the raw request body and
@ -312,7 +312,15 @@ def payment_success():
):
return jsonify({"error": "Invalid or missing payment secret"}), 403
user = g.current_user
# Identify the user from the webhook payload (NOT from client auth)
user_id = data.get("user_id")
if not user_id:
return jsonify({"error": "Missing user_id in webhook payload"}), 400
user = db.session.get(User, user_id)
if not user:
return jsonify({"error": "User not found"}), 404
if not user.has_ai_access:
user.has_ai_access = True
db.session.commit()

View File

@ -546,30 +546,10 @@ $("tab-lobby").onclick = () => switchTab("lobby");
$("close-paywall").onclick = () => paywallModal.classList.add("hidden");
$("unlock-btn").onclick = async () => {
// Generate dummy secret for the stub endpoint
// In production, this would redirect to a real payment gateway (Stripe)
const secret = "change-me-payment-webhook-secret";
const token = localStorage.getItem("sexychat_token");
try {
const resp = await fetch("/api/payment/success", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${token}`
},
body: JSON.stringify({ secret })
});
const res = await resp.json();
if (res.status === "ok") {
// socket event should handle UI unlock, but we can optimistically update
state.hasAiAccess = true;
updateVioletBadge();
paywallModal.classList.add("hidden");
}
} catch (err) {
alert("Payment simulation failed.");
}
// In production, this redirects to a real payment gateway (Stripe Checkout).
// The server-side webhook will unlock AI access after payment confirmation.
// For now, show a placeholder message.
alert("Payment integration coming soon. Contact the administrator to unlock Violet.");
};
logoutBtn.onclick = () => {