feat(hosting): signed preview URLs for POC sites
Replace the standalone POC viewer (duplicate rendering) with signed
JWT preview tokens that bypass StorefrontAccessMiddleware:
Architecture:
1. Admin clicks Preview → route generates signed JWT
2. Redirects to /storefront/{subdomain}/homepage?_preview=token
3. Middleware validates token signature + expiry + store_id
4. Sets request.state.is_preview = True, skips subscription check
5. Full storefront renders with HostWizard preview banner injected
New files:
- app/core/preview_token.py: create_preview_token/verify_preview_token
Changes:
- middleware/storefront_access.py: preview token bypass before sub check
- storefront/base.html: preview banner injection via is_preview state
- hosting/routes/pages/public.py: redirect with signed token (was direct render)
- hosting/routes/api/admin_sites.py: GET /sites/{id}/preview-url endpoint
Removed:
- hosting/templates/hosting/public/poc-viewer.html (replaced by storefront)
Benefits: one rendering path, all section types work, shareable 24h links.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
54
app/core/preview_token.py
Normal file
54
app/core/preview_token.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# app/core/preview_token.py
|
||||
"""
|
||||
Signed preview tokens for POC site previews.
|
||||
|
||||
Generates time-limited JWT tokens that allow viewing storefront pages
|
||||
for stores without active subscriptions (POC sites). The token is
|
||||
validated by StorefrontAccessMiddleware to bypass the subscription gate.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
from jose import JWTError, jwt
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PREVIEW_TOKEN_HOURS = 24
|
||||
ALGORITHM = "HS256"
|
||||
|
||||
|
||||
def create_preview_token(store_id: int, store_code: str, site_id: int) -> str:
|
||||
"""Create a signed preview token for a POC site.
|
||||
|
||||
Token is valid for PREVIEW_TOKEN_HOURS (default 24h) and is tied
|
||||
to a specific store_id. Shareable with clients for preview access.
|
||||
"""
|
||||
payload = {
|
||||
"sub": f"preview:{store_id}",
|
||||
"store_id": store_id,
|
||||
"store_code": store_code,
|
||||
"site_id": site_id,
|
||||
"preview": True,
|
||||
"exp": datetime.now(UTC) + timedelta(hours=PREVIEW_TOKEN_HOURS),
|
||||
"iat": datetime.now(UTC),
|
||||
}
|
||||
return jwt.encode(payload, settings.jwt_secret_key, algorithm=ALGORITHM)
|
||||
|
||||
|
||||
def verify_preview_token(token: str, store_id: int) -> bool:
|
||||
"""Verify a preview token is valid and matches the store.
|
||||
|
||||
Returns True if:
|
||||
- Token signature is valid
|
||||
- Token has not expired
|
||||
- Token has preview=True claim
|
||||
- Token store_id matches the requested store
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(token, settings.jwt_secret_key, algorithms=[ALGORITHM])
|
||||
return payload.get("preview") is True and payload.get("store_id") == store_id
|
||||
except JWTError:
|
||||
return False
|
||||
Reference in New Issue
Block a user