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:

  1. checkout.session.completed: Sets has_physician_access = true
  2. invoice.payment_succeeded: Confirms subscription renewal
  3. invoice.payment_failed: Revokes access (has_physician_access = false)
  4. customer.subscription.deleted: Revokes access after period end
  5. customer.subscription.updated: Updates subscription details

Configuration Files

Main Site Config

File: _quarto.yml

resources:
  - physician-auth-guard.js  # Added for deployment

Physician 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

  1. Unauthenticated User
    • Visit /AtoZ_Physicians/index.html
    • Should redirect to /index.html?login=required
  2. Authenticated User Without Subscription
    • Login with free account
    • Visit /AtoZ_Physicians/index.html
    • Should redirect to /profile.html?subscribe=required
  3. Authenticated User With Active Subscription
    • Login with paid account
    • Visit /AtoZ_Physicians/index.html
    • Should see physician content ✅
  4. Expired Subscription
    • User with subscription_status = 'inactive'
    • Should redirect to subscribe page
  5. Direct URL Access
    • Try accessing /AtoZ_Physicians/exam_prepare.html directly
    • Should still be protected

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 emails

Security 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)

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

  1. “Supabase not initialized” error
    • Check env.js is loaded before auth scripts
    • Verify environment variables in Netlify
  2. Redirect loop
    • Check profile has correct has_physician_access value
    • Verify subscription_status is ‘active’
  3. 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

  1. ✅ Client-side protection implemented
  2. 🔄 Test thoroughly before deployment
  3. 📧 Configure SendGrid for confirmation emails
  4. 🔒 Consider implementing server-side protection
  5. 📊 Set up analytics to track access attempts
  6. 💳 Test subscription cancellation flow
  7. 📱 Test on multiple devices and browsers

Last Updated: November 20, 2025 Status: Ready for testing