Physician Access Protection Setup
Overview
The AtoZ_Physicians section is now protected with client-side authentication that checks for active paid subscriptions before allowing access.
Components
1. Authentication Guard Script
File: physician-auth-guard.js
This script runs on all /AtoZ_Physicians/ pages and: - Checks if user is logged in via Supabase - Verifies user has has_physician_access or physician_access field set to true - Verifies subscription status is active - Redirects unauthorized users appropriately
2. Access Flow
User visits /AtoZ_Physicians/index.html
↓
physician-auth-guard.js loads
↓
Check: Is user logged in?
NO → Redirect to /index.html?login=required
YES ↓
↓
Check: Does user have physician_access = true?
NO → Redirect to /profile.html?subscribe=required
YES ↓
↓
Check: Is subscription_status = 'active'?
NO → Redirect to /profile.html?subscribe=required
YES ↓
↓
✅ Allow access to physician content
3. Redirect Destinations
| Scenario | Redirect To | Purpose |
|---|---|---|
| Not logged in | /index.html?login=required |
User needs to login first |
| No subscription | /profile.html?subscribe=required |
User needs to subscribe |
| Access denied | /access-denied.html |
Catch-all for errors |
4. Access Denied Page
File: access-denied.qmd
Enhanced with three action buttons: - 🎓 Subscribe Now → /profile.html (for users without subscription) - 🔑 Login → /index.html (for users not logged in) - 🏠 Return to Home → / (general navigation)
Database Schema
Required Supabase Tables & Fields
profiles table
- id (uuid, primary key)
- email (text)
- has_physician_access (boolean)
- physician_access (boolean) -- backwards compatibility
- subscription_status (text) -- 'active', 'inactive', 'cancelled'
- stripe_customer_id (text)
- subscription_id (text)
- next_billing_date (timestamp)subscriptions table
- id (text, primary key) -- Stripe subscription ID
- customer (text) -- Stripe customer ID
- profile_id (uuid, foreign key)
- status (text) -- 'active', 'inactive', 'cancelled'
- current_period_start (timestamp)
- current_period_end (timestamp)
- amount (numeric)
- currency (text)
- email (text)purchases table
- user_id (uuid, foreign key)
- status (text) -- 'active', 'inactive', 'failed'
- plan (text) -- 'physicians'
- stripe_session_id (text)Stripe Webhook Integration
The webhook handler (netlify/functions/stripe-webhook.js) handles:
- checkout.session.completed: Sets
has_physician_access = true - invoice.payment_succeeded: Confirms subscription renewal
- invoice.payment_failed: Revokes access (
has_physician_access = false) - customer.subscription.deleted: Revokes access after period end
- customer.subscription.updated: Updates subscription details
Configuration Files
Main Site Config
File: _quarto.yml
resources:
- physician-auth-guard.js # Added for deploymentPhysician Section Config
File: AtoZ_Physicians/_quarto.yml
format:
html:
include-in-header:
- text: |
<script src="../env.js"></script>
<script src="../auth.js"></script>
<script src="../physician-auth-guard.js"></script>Testing Checklist
✅ Test Scenarios
- Unauthenticated User
- Visit
/AtoZ_Physicians/index.html - Should redirect to
/index.html?login=required
- Visit
- Authenticated User Without Subscription
- Login with free account
- Visit
/AtoZ_Physicians/index.html - Should redirect to
/profile.html?subscribe=required
- Authenticated User With Active Subscription
- Login with paid account
- Visit
/AtoZ_Physicians/index.html - Should see physician content ✅
- Expired Subscription
- User with
subscription_status = 'inactive' - Should redirect to subscribe page
- User with
- Direct URL Access
- Try accessing
/AtoZ_Physicians/exam_prepare.htmldirectly - Should still be protected
- Try accessing
Environment Variables
Required in Netlify:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJhbGci...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci... # For webhook
STRIPE_SECRET_KEY=sk_live_... # For webhook
STRIPE_WEBHOOK_SECRET=whsec_... # For webhook validation
STRIPE_PUBLISHABLE_KEY=pk_live_... # For client-side
SENDGRID_API_KEY=SG.xxx # For confirmation emailsSecurity Considerations
⚠️ Client-Side Protection Limitations
- This is client-side protection only
- Determined users can bypass by disabling JavaScript
- For full security, implement server-side protection (Netlify Edge Functions)
🔒 Recommended Enhancements
- Netlify Edge Functions: Add server-side path protection
- Content Encryption: Encrypt sensitive physician content
- Watermarking: Add user-specific watermarks to prevent sharing
- Session Timeout: Implement automatic logout after inactivity
- Device Limit: Restrict concurrent sessions per account
Server-Side Protection (Optional Enhanced Security)
Netlify Edge Function Approach
Create netlify/edge-functions/physician-auth.ts:
import { Context } from "https://edge.netlify.com";
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
// Only protect physician paths
if (!url.pathname.startsWith('/AtoZ_Physicians/')) {
return;
}
// Check for auth cookie
const authCookie = context.cookies.get('sb-access-token');
if (!authCookie) {
return new Response(null, {
status: 302,
headers: { Location: '/index.html?login=required' }
});
}
// Verify token with Supabase (requires edge runtime)
// ... additional server-side verification ...
return; // Allow request to continue
};Deploy Configuration
Add to netlify.toml:
[[edge_functions]]
function = "physician-auth"
path = "/AtoZ_Physicians/*"Support & Troubleshooting
Common Issues
- “Supabase not initialized” error
- Check env.js is loaded before auth scripts
- Verify environment variables in Netlify
- Redirect loop
- Check profile has correct
has_physician_accessvalue - Verify subscription_status is ‘active’
- Check profile has correct
- Access denied for paid users
- Check Stripe webhook is firing correctly
- Verify database fields are being updated
- Check Supabase logs for errors
Debug Mode
Add to browser console:
// Check current user
const { data: { session } } = await window.supabase.auth.getSession();
console.log('User:', session?.user?.email);
// Check profile
const { data: profile } = await window.supabase
.from('profiles')
.select('*')
.eq('id', session.user.id)
.single();
console.log('Profile:', profile);Deployment Checklist
Next Steps
- ✅ Client-side protection implemented
- 🔄 Test thoroughly before deployment
- 📧 Configure SendGrid for confirmation emails
- 🔒 Consider implementing server-side protection
- 📊 Set up analytics to track access attempts
- 💳 Test subscription cancellation flow
- 📱 Test on multiple devices and browsers
Last Updated: November 20, 2025 Status: Ready for testing