Skip to content

Commit 7ff9ed0

Browse files
Feature: Create and integrate auth wrapper for docs (#210)
## Description πŸ“ This PR implements comprehensive OAuth2 authentication for docs by creating an OAuth provider. The authentication system protects all documentation routes while maintaining a seamless user experience with environment-aware configuration that adapts between development and production contexts. ### Authentication Strategy The implementation uses a three-tier approach: 1. **OAuth2 Flow**: Standard authorization code flow with an OAuth2 server 2. **Route Protection**: React context-based authentication state with protected route wrappers 3. **Environment-Aware Access Control**: Mock authentication for development, real GraphQL queries for production ## Code Architecture ### Authentication Context The core authentication logic is centralized in `AuthContext.tsx`: ```typescript export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); const login = () => { initiateLogin(); }; const handleAuthCallbackWrapper = async (code: string, state: string) => { const accessToken = await handleAuthCallback(code, state); storeSession(accessToken); setUser({ id: 'authenticated', email: 'authenticated' }); }; }; ``` ### Route Protection Protected routes are wrapped with authentication checks: ```typescript const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => { const { isAuthenticated, isLoading, login } = useAuth(); if (!isAuthenticated) { return ( <div className="auth-required"> <button onClick={login} className="auth-button"> Sign In with Hasura Cloud </button> </div> ); } return <>{children}</>; }; ``` ### Environment-Aware Configuration The system automatically detects environment context and adapts behavior: ```typescript export const getAuthConfig = (): AuthConfig => { const env = getEnvironmentContext(); if (env.isDevelopment || env.isLocalBuild) { return { useMockUserAccess: true, enableUserAccessCheck: true }; } return { useMockUserAccess: false, graphqlEndpoint: 'https://data.pro.hasura.io/v1/graphql', enableUserAccessCheck: true }; }; ``` ## Production User Access Control In production environments, user entitlements are determined through GraphQL queries against the `ddn_promptql_enabled_users` table, leveraging Hasura's authentication webhook system. The browser automatically includes the `hasura-lux` cookie with GraphQL requests, and Hasura's configured auth webhook decrypts and validates this cookie to authorize the query. ### Access Verification Flow When a user completes the OAuth flow, their access token is stored in a browser cookie. Subsequent GraphQL requests automatically include this cookie, which Hasura's auth webhook validates: ```typescript export const checkUserAccess = async (token: string): Promise<boolean> => { // Store token in cookie for automatic inclusion in requests Cookies.set('hasura-lux', token, { expires: 1 }); const response = await fetch(authConfig.graphqlEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Hasura-Client-Name': 'promptql-docs', }, // Cookie automatically sent by browser body: JSON.stringify({ query: USER_ACCESS_QUERY, variables: {} }) }); const data = await response.json(); const enabledUsers = data.data?.ddn_promptql_enabled_users || []; return enabledUsers.length > 0; }; ``` ### User Entitlement Query The system uses a simple GraphQL query that relies on Hasura's webhook-based authentication to identify the user: ```graphql query CheckUserAccess { ddn_promptql_enabled_users { id email } } ``` The Hasura auth webhook automatically processes the `hasura-lux` cookie sent by the browser, decrypts the user's session data, and determines their identity and permissions before executing the query. ## Developer Changes ### New Scripts - `yarn start` - Starts development environment with authentication services - `yarn dev:stop` - Stops development services - `yarn build:local` - Builds and serves production locally with authentication - Manual service management via `docker compose -f auth/compose.yml` ### Authentication Services The development environment now includes: - **Hydra OAuth2 Server** - **Mock Login/Consent App** - **Docusaurus** ### Environment Setup Docker is now required for development. The setup is automated: ```bash # Start development environment yarn start # Services will be automatically configured: # - Hydra OAuth2 server # - Mock login interface # - OAuth2 client registration ``` ### Testing Authentication Access control can be tested by: 1. Checking out this branch 2. Starting the dev environment (`yarn start`) 3. Visiting any protected documentation page 4. Being redirected to login interface 5. Completing OAuth flow through mock Hasura Cloud login 6. Being redirected back to original destination
1 parent b2550d5 commit 7ff9ed0

37 files changed

+2456
-64
lines changed

β€Ž.gitignoreβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,4 @@ yarn-error.log*
4646
/pdf_output
4747

4848
/static/docs-schema.json
49-
/docs-schema.json
49+
/docs-schema.json

β€ŽDockerfileβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
#Modifying this will trigger deployment without a code change
44
FROM --platform=linux/amd64 node:18.14.2
55

6+
ARG RELEASE_MODE=production
67
ENV PORT=8080
7-
ENV release_mode="production"
8+
ENV release_mode=${RELEASE_MODE}
89
ENV BUILD_VERSION="3.0"
910

1011
# Create app directory

β€ŽREADME.mdβ€Ž

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ git clone https://github.com/hasura/promptql-docs.git
2727
cd promptql-docs
2828
yarn
2929

30-
# Start the dev server
30+
# Start the development environment
3131
yarn start
3232
```
3333

34-
The docs will open at `http://localhost:3001` with hot reloading - edit away and see changes instantly.
34+
The docs will open at `http://localhost:3001` with hot reloading and full authentication - edit away and see changes instantly.
3535

3636
### Making Changes
3737

@@ -45,11 +45,11 @@ The docs will open at `http://localhost:3001` with hot reloading - edit away and
4545
# Build for production
4646
yarn build
4747

48-
# Test the build locally
49-
yarn serve
48+
# Test the production build locally (with authentication)
49+
yarn build:local
5050
```
5151

52-
The build generates static files in the `build` directory, ready for any static hosting service.
52+
The `build` command generates static files in the `build` directory, ready for any static hosting service. The `build:local` command builds the site and serves it locally with the full authentication system for testing.
5353

5454

5555
## Need Help?

β€Žauth/README.mdβ€Ž

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# PromptQL Docs Authentication Setup
2+
3+
This directory contains the mock authentication setup for the PromptQL documentation site using Hydra OAuth2 server.
4+
5+
## Quick Start
6+
7+
1. **Start the development environment:**
8+
```bash
9+
yarn start
10+
```
11+
12+
2. **Test the authentication flow:**
13+
- Visit http://localhost:3001
14+
- Try to access any documentation page (e.g., http://localhost:3001/docs/quickstart/)
15+
- You should be redirected to the login page
16+
- Click "Sign In with Hasura Cloud" to start the OAuth flow
17+
18+
3. **Test production build locally:**
19+
```bash
20+
yarn build:local
21+
```
22+
23+
## Architecture Overview
24+
25+
### Components
26+
27+
1. **Hydra OAuth2 Server** (Port 4444/4445)
28+
- Handles OAuth2 authorization flow
29+
- Admin API on port 4445, Public API on port 4444
30+
31+
2. **Login/Consent App** (Port 3000)
32+
- Mock login interface that simulates Hasura Cloud login
33+
- Handles user consent for OAuth2 flow
34+
35+
3. **Docusaurus App** (Port 3001)
36+
- Main documentation site with integrated authentication
37+
38+
### Authentication Flow
39+
40+
1. User visits protected documentation page
41+
2. `Root.tsx` provides AuthContext to entire app
42+
3. `Layout.tsx` component checks authentication status
43+
4. If not authenticated, `ProtectedRoute.tsx` shows login prompt
44+
5. User clicks login β†’ redirected to Hydra OAuth2 endpoint
45+
6. Hydra redirects to mock login app (port 3000)
46+
7. User "logs in" β†’ redirected back to Hydra
47+
8. Hydra redirects to `/docs/callback` with authorization code
48+
9. `AuthContext.tsx` exchanges code for access token (with duplicate call protection)
49+
10. User info is fetched and stored in cookies
50+
11. User is redirected to original destination
51+
52+
### Key Files
53+
54+
- `src/contexts/AuthContext.tsx` - Main authentication logic with OAuth2 flow
55+
- `src/theme/Root.tsx` - Provides AuthContext to entire app
56+
- `src/theme/Layout/index.tsx` - Route protection logic and loading states
57+
- `src/components/ProtectedRoute.tsx` - Protected route wrapper with delayed loading
58+
- `src/pages/login.tsx` - Login page with brand styling
59+
- `src/pages/callback.tsx` - OAuth callback handler with duplicate call protection
60+
- `src/theme/Navbar/Content/index.tsx` - Navbar with conditional auth elements
61+
- `src/css/custom.css` - Authentication styles with brand colors and animations
62+
63+
## Configuration
64+
65+
### OAuth2 Settings
66+
- **Client ID:** `docusaurus-client`
67+
- **Redirect URI:** `http://localhost:3001/docs/callback`
68+
- **Scopes:** `openid email`
69+
70+
### Public Routes
71+
The following routes are accessible without authentication:
72+
- `/` (landing page)
73+
- `/docs/` (docs landing page)
74+
- `/login` (login page)
75+
- `/docs/callback` (OAuth callback)
76+
77+
All other routes require authentication.
78+
79+
## Key Features
80+
81+
### UI/UX Enhancements
82+
- **Brand Colors**: Uses PromptQL brand colors that adapt to light/dark themes
83+
- **Delayed Loading**: Loading animations only appear after 5 seconds to avoid visual noise
84+
- **Animated Dots**: Professional 3-dot loading animation using CSS keyframes
85+
- **Conditional Navbar**: Search and navigation elements hidden when unauthenticated
86+
- **Responsive Design**: Works across all device sizes
87+
88+
### Technical Features
89+
- **Duplicate Call Protection**: Prevents React Strict Mode from causing "code already used" errors
90+
- **CORS Support**: Proper CORS configuration for browser-to-Hydra communication
91+
- **Session Persistence**: Authentication state persists across browser refreshes
92+
- **Error Handling**: Comprehensive error states with user-friendly messages
93+
- **Clean Architecture**: Centralized auth logic with proper separation of concerns
94+
95+
## Development vs Production
96+
97+
### Development (Current Setup)
98+
- Mock Hydra server with in-memory storage and CORS enabled
99+
- Mock login/consent interface configured as public client (no client secret)
100+
- User access check always returns `true`
101+
- Access tokens stored in browser cookies (`pql-docs-access`) with 1-day expiration; refresh tokens in (`pql-docs-refresh`)
102+
- Tokens are retrieved from cookies and sent as Authorization Bearer headers to GraphQL API
103+
- Delayed loading animations (5-second threshold)
104+
- Brand-consistent styling across light/dark themes
105+
106+
### Production (Future)
107+
- Real Hydra endpoints: `https://oauth.pro.hasura.io/*`
108+
- Real Hasura Cloud login interface configured as public client
109+
- GraphQL API check against `ddn_promptql_enabled_users` table
110+
- Secure httpOnly cookies for token storage
111+
- Tokens sent as Authorization Bearer headers to GraphQL API
112+
- Proper error handling and security headers
113+
114+
## Troubleshooting
115+
116+
### Services won't start
117+
```bash
118+
# Check if ports are in use
119+
lsof -i :3000 -i :4444 -i :4445
120+
121+
# Stop existing containers
122+
docker compose -f auth/compose.yml down
123+
124+
# Restart services
125+
./auth/start-auth-dev.sh
126+
```
127+
128+
### Authentication fails
129+
```bash
130+
# Check service logs
131+
docker compose -f auth/compose.yml logs -f
132+
133+
# Verify client registration
134+
curl http://localhost:4445/admin/clients/docusaurus-client
135+
```
136+
137+
### Clear authentication state
138+
```bash
139+
# In browser console:
140+
document.cookie.split(";").forEach(function(c) {
141+
document.cookie = c.replace(/^ +/, "").replace(/=.*/, "=;expires=" + new Date().toUTCString() + ";path=/");
142+
});
143+
```
144+
145+
## Testing Checklist
146+
147+
- [ ] Unauthenticated users can access landing pages (`/` and `/docs/`)
148+
- [ ] Unauthenticated users redirected to login for protected pages
149+
- [ ] Login flow redirects to Hydra auth endpoint
150+
- [ ] Mock login interface appears and works correctly
151+
- [ ] Successful auth redirects to `/docs/callback` then to original destination
152+
- [ ] User email appears in navbar after authentication
153+
- [ ] Logout clears session and redirects to `/docs/index/`
154+
- [ ] Auth state persists across browser refresh
155+
- [ ] Loading animations only appear after 5-second delay
156+
- [ ] Search bar and navigation hidden when unauthenticated
157+
- [ ] Error handling works for failed auth attempts
158+
- [ ] No "authorization code already used" errors (duplicate call protection)
159+
160+
## Commands
161+
162+
```bash
163+
# Start development environment
164+
yarn start
165+
166+
# Stop development services
167+
yarn dev:stop
168+
169+
# Test production build locally
170+
yarn build:local
171+
172+
# Manual service management (if needed)
173+
docker compose -f auth/compose.yml logs -f # View logs
174+
docker compose -f auth/compose.yml down # Stop services
175+
docker compose -f auth/compose.yml restart hydra # Restart Hydra
176+
177+
# Check client configuration
178+
curl http://localhost:4445/admin/clients/docusaurus-client
179+
```

β€Žauth/compose.ymlβ€Ž

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
services:
2+
hydra-migrate:
3+
image: oryd/hydra:v2.2.0
4+
environment:
5+
- DSN=memory
6+
command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes
7+
volumes:
8+
- type: bind
9+
source: ./hydra
10+
target: /etc/config/hydra
11+
restart: on-failure
12+
13+
hydra:
14+
image: oryd/hydra:v2.2.0
15+
ports:
16+
- "4444:4444" # Public port
17+
- "4445:4445" # Admin port
18+
command: serve -c /etc/config/hydra/hydra.yml all --dev
19+
volumes:
20+
- type: bind
21+
source: ./hydra
22+
target: /etc/config/hydra
23+
environment:
24+
- DSN=memory
25+
restart: unless-stopped
26+
depends_on:
27+
- hydra-migrate
28+
29+
# Mock consent/login app
30+
hydra-login-consent:
31+
image: oryd/hydra-login-consent-node:v2.2.0
32+
ports:
33+
- "3000:3000"
34+
environment:
35+
- HYDRA_ADMIN_URL=http://hydra:4445
36+
- NODE_TLS_REJECT_UNAUTHORIZED=0
37+
restart: unless-stopped
38+
depends_on:
39+
- hydra

β€Žauth/hydra/hydra.ymlβ€Ž

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
serve:
2+
public:
3+
port: 4444
4+
host: 0.0.0.0
5+
cors:
6+
enabled: true
7+
allowed_origins:
8+
- http://localhost:3001
9+
allowed_methods:
10+
- POST
11+
- GET
12+
- PUT
13+
- PATCH
14+
- DELETE
15+
allowed_headers:
16+
- Authorization
17+
- Content-Type
18+
allow_credentials: true
19+
admin:
20+
port: 4445
21+
host: 0.0.0.0
22+
23+
urls:
24+
self:
25+
issuer: http://localhost:4444
26+
consent: http://localhost:3000/consent
27+
login: http://localhost:3000/login
28+
29+
secrets:
30+
system:
31+
- youReallyNeedToChangeThis
32+
33+
oauth2:
34+
expose_internal_errors: true
35+
36+
log:
37+
level: debug

β€Žauth/setup-client.shβ€Ž

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
echo "Waiting for Hydra to start..."
4+
sleep 10
5+
6+
# Create OAuth2 client for Docusaurus
7+
curl -X POST \
8+
http://localhost:4445/admin/clients \
9+
-H "Content-Type: application/json" \
10+
-d '{
11+
"client_id": "docusaurus-client",
12+
"client_name": "Docusaurus Documentation",
13+
"redirect_uris": ["http://localhost:3001/docs/callback"],
14+
"grant_types": ["authorization_code"],
15+
"response_types": ["code"],
16+
"scope": "openid email",
17+
"token_endpoint_auth_method": "none"
18+
}'
19+
20+
echo "Client created successfully!"
21+
echo "Client ID: docusaurus-client"
22+
echo "Client Type: Public (no client secret)"
23+
echo "Redirect URI: http://localhost:3001/docs/callback"

β€Žauth/start-auth-dev.shβ€Ž

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/bin/bash
2+
3+
echo "πŸš€ Starting PromptQL Docs Authentication Development Environment"
4+
echo "=============================================================="
5+
6+
# Check if Docker is running
7+
if ! docker info > /dev/null 2>&1; then
8+
echo "❌ Docker is not running. Please start Docker and try again."
9+
exit 1
10+
fi
11+
12+
echo "πŸ“¦ Starting Hydra and Login/Consent services..."
13+
14+
# Start the services in the background
15+
cd "$(dirname "$0")"
16+
docker compose -f compose.yml up -d
17+
18+
echo "⏳ Waiting for services to start..."
19+
sleep 15
20+
21+
echo "πŸ”§ Setting up OAuth2 client..."
22+
./setup-client.sh
23+
24+
echo ""
25+
echo "βœ… Authentication services are ready!"
26+
echo ""
27+
echo "🌐 Service URLs:"
28+
echo " - Hydra Admin API: http://localhost:4445"
29+
echo " - Hydra Public API: http://localhost:4444"
30+
echo " - Login/Consent UI: http://localhost:3000"
31+
echo ""
32+
echo "πŸ“ Next steps:"
33+
echo " 1. Start your Docusaurus dev server: npm start"
34+
echo " 2. Visit http://localhost:3001"
35+
echo " 3. Try accessing any documentation page to test authentication"
36+
echo ""
37+
echo "πŸ” To view logs:"
38+
echo " docker compose -f auth/compose.yml logs -f"
39+
echo ""
40+
echo "πŸ›‘ To stop services:"
41+
echo " docker compose -f auth/compose.yml down"

β€Žcustom-build-script.cjsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function generateLlmBundle() {
7070

7171
// First run the Docusaurus build
7272
console.log('\n\x1b[36mRunning Docusaurus build...\x1b[0m');
73-
execSync('docusaurus build', { cwd: rootDir, stdio: 'inherit' });
73+
execSync('docusaurus build', { cwd: rootDir, stdio: 'inherit', env: process.env });
7474

7575
// Then try to generate LLM bundle
7676
try {

0 commit comments

Comments
Β (0)