From e7f6e9c20a060783f1116bacef26df6e6fbf3adc Mon Sep 17 00:00:00 2001 From: Andika Date: Mon, 2 Feb 2026 17:52:25 +0700 Subject: [PATCH] Update Stripe publishable key storage in Stripe Settings 1. Updated UpdateStripeSettingsRequest - Added publishable_key field 2. Updated update_stripe_settings endpoint - Now validates and stores: - stripe_publishable_key (not encrypted - it's public) - stripe_secret_key (encrypted) - stripe_webhook_secret (encrypted) - Also validates that publishable and secret keys are from the same environment (both test or both live) 3. Added new public endpoint GET /api/config/stripe - Returns the publishable key to the frontend (no auth required since it's meant to be public) 4. Updated get_stripe_status endpoint - Now includes publishable_key_prefix and publishable_key_set in the response --- __pycache__/server.cpython-312.pyc | Bin 394626 -> 396746 bytes server.py | 58 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc index 7eb827466589d23af089e82c667db4f01ed95601..0c295b80153e70acdaff12c07a8d386ce2e1e9f4 100644 GIT binary patch delta 5524 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 P)HJXbj|qRUG|cjU!cGv5 diff --git a/server.py b/server.py index 71530b4..2aaf852 100644 --- a/server.py +++ b/server.py @@ -8395,6 +8395,29 @@ def set_setting( db.commit() +@api_router.get("/config/stripe") +async def get_stripe_public_config(db: Session = Depends(get_db)): + """ + Get Stripe publishable key for frontend (public endpoint). + + This endpoint provides the publishable key needed for Stripe.js + to initialize payment forms. No authentication required since + publishable keys are meant to be public. + """ + publishable_key = get_setting(db, 'stripe_publishable_key', decrypt=False) + + if not publishable_key: + raise HTTPException( + status_code=503, + detail="Stripe is not configured. Please contact the administrator." + ) + + return { + "publishable_key": publishable_key, + "environment": "test" if publishable_key.startswith('pk_test_') else "live" + } + + @api_router.get("/admin/settings/stripe/status") async def get_stripe_status( current_user: User = Depends(get_current_superadmin), @@ -8405,6 +8428,7 @@ async def get_stripe_status( Returns: - configured: Whether credentials exist in database + - publishable_key_prefix: First 12 chars of publishable key - secret_key_prefix: First 10 chars of secret key (for verification) - webhook_configured: Whether webhook secret exists - environment: test or live (based on key prefix) @@ -8413,10 +8437,11 @@ async def get_stripe_status( import os # Read from database + publishable_key = get_setting(db, 'stripe_publishable_key', decrypt=False) secret_key = get_setting(db, 'stripe_secret_key', decrypt=True) webhook_secret = get_setting(db, 'stripe_webhook_secret', decrypt=True) - configured = bool(secret_key) + configured = bool(secret_key) and bool(publishable_key) environment = 'unknown' if secret_key: @@ -8436,6 +8461,8 @@ async def get_stripe_status( return { "configured": configured, + "publishable_key_prefix": publishable_key[:12] if publishable_key else None, + "publishable_key_set": bool(publishable_key), "secret_key_prefix": secret_key[:10] if secret_key else None, "secret_key_set": bool(secret_key), "webhook_secret_set": bool(webhook_secret), @@ -8444,6 +8471,7 @@ async def get_stripe_status( "instructions": { "location": "Database (system_settings table)", "required_settings": [ + "stripe_publishable_key (pk_test_... or pk_live_...)", "stripe_secret_key (sk_test_... or sk_live_...)", "stripe_webhook_secret (whsec_...)" ], @@ -8501,6 +8529,7 @@ async def test_stripe_connection( class UpdateStripeSettingsRequest(BaseModel): """Request model for updating Stripe settings""" + publishable_key: str = Field(..., min_length=1, description="Stripe publishable key (pk_test_... or pk_live_...)") secret_key: str = Field(..., min_length=1, description="Stripe secret key (sk_test_... or sk_live_...)") webhook_secret: str = Field(..., min_length=1, description="Stripe webhook secret (whsec_...)") @@ -8517,6 +8546,13 @@ async def update_stripe_settings( Stores Stripe credentials encrypted in the database. Changes take effect immediately without server restart. """ + # Validate publishable key format + if not (request.publishable_key.startswith('pk_test_') or request.publishable_key.startswith('pk_live_')): + raise HTTPException( + status_code=400, + detail="Invalid Stripe publishable key format. Must start with 'pk_test_' or 'pk_live_'" + ) + # Validate secret key format if not (request.secret_key.startswith('sk_test_') or request.secret_key.startswith('sk_live_')): raise HTTPException( @@ -8531,7 +8567,27 @@ async def update_stripe_settings( detail="Invalid Stripe webhook secret format. Must start with 'whsec_'" ) + # Validate key environment consistency (publishable and secret should match) + pk_is_live = request.publishable_key.startswith('pk_live_') + sk_is_live = request.secret_key.startswith('sk_live_') + if pk_is_live != sk_is_live: + raise HTTPException( + status_code=400, + detail="Publishable key and Secret key must be from the same environment (both test or both live)" + ) + try: + # Store publishable key (NOT encrypted - it's meant to be public) + set_setting( + db=db, + key='stripe_publishable_key', + value=request.publishable_key, + user_id=str(current_user.id), + description='Stripe publishable key for frontend payment forms', + is_sensitive=False, + encrypt=False + ) + # Store secret key (encrypted) set_setting( db=db,