Initial commit
This commit is contained in:
210
templates/cloudflare/worker-auth.ts
Normal file
210
templates/cloudflare/worker-auth.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Cloudflare Worker with Clerk Authentication
|
||||
*
|
||||
* This template demonstrates:
|
||||
* - JWT token verification with @clerk/backend
|
||||
* - Protected API routes
|
||||
* - Type-safe Hono context with auth state
|
||||
* - Proper error handling
|
||||
*
|
||||
* Dependencies:
|
||||
* - @clerk/backend@^2.17.2
|
||||
* - hono@^4.10.1
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono'
|
||||
import { verifyToken } from '@clerk/backend'
|
||||
import { cors } from 'hono/cors'
|
||||
|
||||
// Type-safe environment bindings
|
||||
type Bindings = {
|
||||
CLERK_SECRET_KEY: string
|
||||
CLERK_PUBLISHABLE_KEY: string
|
||||
}
|
||||
|
||||
// Context variables with auth state
|
||||
type Variables = {
|
||||
userId: string | null
|
||||
sessionClaims: any | null
|
||||
}
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
||||
|
||||
// CORS middleware (adjust origins for production)
|
||||
app.use('*', cors({
|
||||
origin: ['http://localhost:5173', 'https://yourdomain.com'],
|
||||
credentials: true,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Auth Middleware - Verifies Clerk JWT tokens
|
||||
*
|
||||
* CRITICAL SECURITY:
|
||||
* - Always set authorizedParties to prevent CSRF attacks
|
||||
* - Use secretKey, not deprecated apiKey
|
||||
* - Token is in Authorization: Bearer <token> header
|
||||
*/
|
||||
app.use('/api/*', async (c, next) => {
|
||||
const authHeader = c.req.header('Authorization')
|
||||
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
// No auth header - continue as unauthenticated
|
||||
c.set('userId', null)
|
||||
c.set('sessionClaims', null)
|
||||
return next()
|
||||
}
|
||||
|
||||
// Extract token from "Bearer <token>"
|
||||
const token = authHeader.substring(7)
|
||||
|
||||
try {
|
||||
// Verify token with Clerk
|
||||
const { data, error } = await verifyToken(token, {
|
||||
secretKey: c.env.CLERK_SECRET_KEY,
|
||||
|
||||
// IMPORTANT: Set to your actual domain(s) to prevent CSRF
|
||||
// Source: https://clerk.com/docs/reference/backend/verify-token
|
||||
authorizedParties: [
|
||||
'http://localhost:5173', // Development
|
||||
'https://yourdomain.com', // Production
|
||||
],
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('[Auth] Token verification failed:', error.message)
|
||||
c.set('userId', null)
|
||||
c.set('sessionClaims', null)
|
||||
} else {
|
||||
// 'sub' claim contains the user ID
|
||||
c.set('userId', data.sub)
|
||||
c.set('sessionClaims', data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Auth] Token verification error:', err)
|
||||
c.set('userId', null)
|
||||
c.set('sessionClaims', null)
|
||||
}
|
||||
|
||||
return next()
|
||||
})
|
||||
|
||||
/**
|
||||
* Public Routes - No authentication required
|
||||
*/
|
||||
|
||||
app.get('/api/public', (c) => {
|
||||
return c.json({
|
||||
message: 'This endpoint is public',
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/health', (c) => {
|
||||
return c.json({
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Protected Routes - Require authentication
|
||||
*/
|
||||
|
||||
app.get('/api/protected', (c) => {
|
||||
const userId = c.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: 'This endpoint is protected',
|
||||
userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/api/user/profile', (c) => {
|
||||
const userId = c.get('userId')
|
||||
const sessionClaims = c.get('sessionClaims')
|
||||
|
||||
if (!userId) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
// Access custom claims from JWT template (if configured)
|
||||
return c.json({
|
||||
userId,
|
||||
email: sessionClaims?.email,
|
||||
role: sessionClaims?.role,
|
||||
organizationId: sessionClaims?.organization_id,
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* POST Example - Create resource with auth
|
||||
*/
|
||||
|
||||
app.post('/api/items', async (c) => {
|
||||
const userId = c.get('userId')
|
||||
|
||||
if (!userId) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
const body = await c.req.json()
|
||||
|
||||
// Validate and process body
|
||||
// Example: save to D1, KV, or R2
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
itemId: crypto.randomUUID(),
|
||||
userId,
|
||||
}, 201)
|
||||
})
|
||||
|
||||
/**
|
||||
* Role-Based Access Control Example
|
||||
*/
|
||||
|
||||
app.get('/api/admin/dashboard', (c) => {
|
||||
const userId = c.get('userId')
|
||||
const sessionClaims = c.get('sessionClaims')
|
||||
|
||||
if (!userId) {
|
||||
return c.json({ error: 'Unauthorized' }, 401)
|
||||
}
|
||||
|
||||
// Check role from custom JWT claims
|
||||
const role = sessionClaims?.role
|
||||
|
||||
if (role !== 'admin') {
|
||||
return c.json({ error: 'Forbidden: Admin access required' }, 403)
|
||||
}
|
||||
|
||||
return c.json({
|
||||
message: 'Admin dashboard data',
|
||||
userId,
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Error Handling
|
||||
*/
|
||||
|
||||
app.onError((err, c) => {
|
||||
console.error('[Error]', err)
|
||||
return c.json({ error: 'Internal Server Error' }, 500)
|
||||
})
|
||||
|
||||
app.notFound((c) => {
|
||||
return c.json({ error: 'Not Found' }, 404)
|
||||
})
|
||||
|
||||
/**
|
||||
* Export the Hono app
|
||||
*
|
||||
* ES Module format for Cloudflare Workers
|
||||
*/
|
||||
export default app
|
||||
46
templates/cloudflare/wrangler.jsonc
Normal file
46
templates/cloudflare/wrangler.jsonc
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "my-clerk-worker",
|
||||
"main": "src/index.ts",
|
||||
"account_id": "YOUR_ACCOUNT_ID",
|
||||
"compatibility_date": "2025-10-11",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
},
|
||||
"vars": {
|
||||
"CLERK_PUBLISHABLE_KEY": "pk_test_..."
|
||||
}
|
||||
|
||||
/**
|
||||
* CRITICAL: Never commit CLERK_SECRET_KEY to version control
|
||||
*
|
||||
* To set CLERK_SECRET_KEY:
|
||||
*
|
||||
* 1. Production:
|
||||
* wrangler secret put CLERK_SECRET_KEY
|
||||
*
|
||||
* 2. Local development:
|
||||
* Create .dev.vars file (see templates/env-examples/.dev.vars.example)
|
||||
*
|
||||
* After setting, access via c.env.CLERK_SECRET_KEY in your Worker
|
||||
*/
|
||||
|
||||
/**
|
||||
* Optional: Add other Cloudflare bindings
|
||||
*
|
||||
* KV Namespace:
|
||||
* "kv_namespaces": [
|
||||
* { "binding": "AUTH_CACHE", "id": "YOUR_KV_ID" }
|
||||
* ]
|
||||
*
|
||||
* D1 Database:
|
||||
* "d1_databases": [
|
||||
* { "binding": "DB", "database_name": "my-db", "database_id": "YOUR_DB_ID" }
|
||||
* ]
|
||||
*
|
||||
* R2 Bucket:
|
||||
* "r2_buckets": [
|
||||
* { "binding": "ASSETS", "bucket_name": "my-bucket" }
|
||||
* ]
|
||||
*/
|
||||
}
|
||||
63
templates/env-examples/.dev.vars.example
Normal file
63
templates/env-examples/.dev.vars.example
Normal file
@@ -0,0 +1,63 @@
|
||||
# Clerk Environment Variables for Cloudflare Workers
|
||||
#
|
||||
# Copy this file to .dev.vars for local development
|
||||
# Get your keys from https://dashboard.clerk.com
|
||||
|
||||
# ==========================================
|
||||
# LOCAL DEVELOPMENT (.dev.vars)
|
||||
# ==========================================
|
||||
|
||||
# Secret Key (server-side verification)
|
||||
CLERK_SECRET_KEY=sk_test_...
|
||||
|
||||
# Publishable Key (can be in wrangler.jsonc or here)
|
||||
CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
|
||||
# ==========================================
|
||||
# PRODUCTION DEPLOYMENT
|
||||
# ==========================================
|
||||
|
||||
# For production, use wrangler secrets:
|
||||
#
|
||||
# 1. Set secret key (encrypted, not in wrangler.jsonc):
|
||||
# wrangler secret put CLERK_SECRET_KEY
|
||||
#
|
||||
# 2. Set publishable key in wrangler.jsonc:
|
||||
# {
|
||||
# "vars": {
|
||||
# "CLERK_PUBLISHABLE_KEY": "pk_live_..."
|
||||
# }
|
||||
# }
|
||||
|
||||
# ==========================================
|
||||
# SECURITY NOTES
|
||||
# ==========================================
|
||||
|
||||
# 1. NEVER commit .dev.vars to version control
|
||||
# .dev.vars is in .gitignore by default
|
||||
#
|
||||
# 2. Use different keys for development and production
|
||||
# - Development: pk_test_... / sk_test_...
|
||||
# - Production: pk_live_... / sk_live_...
|
||||
#
|
||||
# 3. Production secrets via wrangler secret put
|
||||
# This encrypts secrets, they won't appear in wrangler.jsonc
|
||||
#
|
||||
# 4. Rotate CLERK_SECRET_KEY if compromised
|
||||
# Generate new keys in Clerk Dashboard
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Additional Bindings
|
||||
# ==========================================
|
||||
|
||||
# If your Worker uses other services, add them here:
|
||||
# DATABASE_URL=...
|
||||
# API_KEY=...
|
||||
|
||||
# ==========================================
|
||||
# REFERENCE
|
||||
# ==========================================
|
||||
|
||||
# Official Docs:
|
||||
# https://clerk.com/docs/reference/backend/verify-token
|
||||
# https://developers.cloudflare.com/workers/wrangler/configuration/
|
||||
89
templates/env-examples/.env.local.example
Normal file
89
templates/env-examples/.env.local.example
Normal file
@@ -0,0 +1,89 @@
|
||||
# Clerk Environment Variables for Next.js
|
||||
#
|
||||
# Copy this file to .env.local and fill in your actual values
|
||||
# Get your keys from https://dashboard.clerk.com
|
||||
|
||||
# ==========================================
|
||||
# REQUIRED
|
||||
# ==========================================
|
||||
|
||||
# Publishable Key (safe to expose to client)
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
|
||||
# Secret Key (NEVER expose to client, server-side only)
|
||||
CLERK_SECRET_KEY=sk_test_...
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Custom Pages
|
||||
# ==========================================
|
||||
|
||||
# Uncomment to use custom sign-in/sign-up pages
|
||||
# NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||
# NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Redirect URLs
|
||||
# ==========================================
|
||||
|
||||
# Where to redirect after sign-in (forced - always goes here)
|
||||
# NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
|
||||
|
||||
# Where to redirect after sign-up (forced - always goes here)
|
||||
# NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/onboarding
|
||||
|
||||
# Fallback redirect if no forced redirect is set (default: /)
|
||||
# NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
|
||||
# NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Webhooks
|
||||
# ==========================================
|
||||
|
||||
# Webhook signing secret for verifying Clerk webhooks
|
||||
# Get this from Clerk Dashboard > Webhooks > Add Endpoint
|
||||
# CLERK_WEBHOOK_SIGNING_SECRET=whsec_...
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Multi-Domain (Satellite Domains)
|
||||
# ==========================================
|
||||
|
||||
# For multi-domain authentication
|
||||
# NEXT_PUBLIC_CLERK_DOMAIN=accounts.yourdomain.com
|
||||
# NEXT_PUBLIC_CLERK_IS_SATELLITE=true
|
||||
|
||||
# ==========================================
|
||||
# OPTIONAL - Advanced Configuration
|
||||
# ==========================================
|
||||
|
||||
# Custom Clerk JS URL (usually not needed)
|
||||
# NEXT_PUBLIC_CLERK_JS_URL=https://...
|
||||
|
||||
# Proxy URL for requests (enterprise feature)
|
||||
# NEXT_PUBLIC_CLERK_PROXY_URL=https://...
|
||||
|
||||
# Disable telemetry
|
||||
# CLERK_TELEMETRY_DISABLED=1
|
||||
|
||||
# ==========================================
|
||||
# SECURITY NOTES
|
||||
# ==========================================
|
||||
|
||||
# 1. NEVER commit .env.local to version control
|
||||
# Add .env.local to .gitignore
|
||||
#
|
||||
# 2. Use different keys for development and production
|
||||
# - Development: pk_test_... / sk_test_...
|
||||
# - Production: pk_live_... / sk_live_...
|
||||
#
|
||||
# 3. NEVER use NEXT_PUBLIC_ prefix for secrets
|
||||
# NEXT_PUBLIC_ variables are exposed to the browser
|
||||
#
|
||||
# 4. Rotate CLERK_SECRET_KEY if compromised
|
||||
# Generate new keys in Clerk Dashboard
|
||||
|
||||
# ==========================================
|
||||
# REFERENCE
|
||||
# ==========================================
|
||||
|
||||
# Official Docs:
|
||||
# https://clerk.com/docs/guides/development/clerk-environment-variables
|
||||
46
templates/env-examples/.env.local.vite.example
Normal file
46
templates/env-examples/.env.local.vite.example
Normal file
@@ -0,0 +1,46 @@
|
||||
# Clerk Environment Variables for React + Vite
|
||||
#
|
||||
# Copy this file to .env.local and fill in your actual values
|
||||
# Get your keys from https://dashboard.clerk.com
|
||||
|
||||
# ==========================================
|
||||
# REQUIRED
|
||||
# ==========================================
|
||||
|
||||
# Publishable Key (safe to expose to client)
|
||||
# CRITICAL: Must use VITE_ prefix for Vite to expose to client
|
||||
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
|
||||
# ==========================================
|
||||
# SECURITY NOTES
|
||||
# ==========================================
|
||||
|
||||
# 1. NEVER commit .env.local to version control
|
||||
# Add .env.local to .gitignore
|
||||
#
|
||||
# 2. Must use VITE_ prefix for client-side variables
|
||||
# Without VITE_ prefix, variable won't be available
|
||||
#
|
||||
# 3. Only VITE_ prefixed vars are exposed to browser
|
||||
# Never use VITE_ prefix for secrets
|
||||
#
|
||||
# 4. Restart dev server after changing .env.local
|
||||
# Vite only reads env vars on startup
|
||||
#
|
||||
# 5. Use different keys for development and production
|
||||
# - Development: pk_test_...
|
||||
# - Production: pk_live_...
|
||||
|
||||
# ==========================================
|
||||
# ACCESS IN CODE
|
||||
# ==========================================
|
||||
|
||||
# Use import.meta.env to access:
|
||||
# const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
|
||||
|
||||
# ==========================================
|
||||
# REFERENCE
|
||||
# ==========================================
|
||||
|
||||
# Vite Env Vars: https://vitejs.dev/guide/env-and-mode.html
|
||||
# Clerk Docs: https://clerk.com/docs/references/react/clerk-provider
|
||||
23
templates/jwt/advanced-template.json
Normal file
23
templates/jwt/advanced-template.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$comment": "Advanced Clerk JWT Template - Multi-Tenant with Fallbacks",
|
||||
"$description": "This template demonstrates advanced features: string interpolation, conditional expressions, nested metadata access, and organization claims. Copy this JSON (without $ prefixed fields) into Clerk Dashboard.",
|
||||
|
||||
"user_id": "{{user.id}}",
|
||||
"email": "{{user.primary_email_address}}",
|
||||
"full_name": "{{user.last_name}} {{user.first_name}}",
|
||||
"avatar": "{{user.image_url}}",
|
||||
|
||||
"role": "{{user.public_metadata.role || 'user'}}",
|
||||
"department": "{{user.public_metadata.department || 'general'}}",
|
||||
"permissions": "{{user.public_metadata.permissions}}",
|
||||
|
||||
"org_id": "{{user.public_metadata.org_id}}",
|
||||
"org_slug": "{{user.public_metadata.org_slug}}",
|
||||
"org_role": "{{user.public_metadata.org_role}}",
|
||||
|
||||
"interests": "{{user.public_metadata.profile.interests}}",
|
||||
"has_verified_contact": "{{user.email_verified || user.phone_number_verified}}",
|
||||
|
||||
"age": "{{user.public_metadata.age || user.unsafe_metadata.age || 18}}",
|
||||
"onboarding_complete": "{{user.public_metadata.onboardingComplete || false}}"
|
||||
}
|
||||
8
templates/jwt/basic-template.json
Normal file
8
templates/jwt/basic-template.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$comment": "Basic Clerk JWT Template - Role-Based Access Control",
|
||||
"$description": "This template includes minimal user information for role-based authentication. Copy this JSON (without comments) into Clerk Dashboard > Sessions > Customize session token > Create template.",
|
||||
|
||||
"user_id": "{{user.id}}",
|
||||
"email": "{{user.primary_email_address}}",
|
||||
"role": "{{user.public_metadata.role || 'user'}}"
|
||||
}
|
||||
14
templates/jwt/grafbase-template.json
Normal file
14
templates/jwt/grafbase-template.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$comment": "Grafbase GraphQL Integration JWT Template",
|
||||
"$description": "This template is for Grafbase integration with role-based access control. Grafbase uses 'groups' array for authorization. Name it 'grafbase' in Clerk Dashboard.",
|
||||
"$usage": "const token = await getToken({ template: 'grafbase' }); // Use in GraphQL requests",
|
||||
"$grafbase_config": "In grafbase.toml: [auth.providers.clerk] issuer = 'https://your-app.clerk.accounts.dev' jwks = 'https://your-app.clerk.accounts.dev/.well-known/jwks.json'",
|
||||
|
||||
"sub": "{{user.id}}",
|
||||
"groups": [
|
||||
"org:{{user.public_metadata.org_role || 'member'}}",
|
||||
"user:authenticated"
|
||||
],
|
||||
"email": "{{user.primary_email_address}}",
|
||||
"name": "{{user.full_name || user.first_name}}"
|
||||
}
|
||||
17
templates/jwt/supabase-template.json
Normal file
17
templates/jwt/supabase-template.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$comment": "Supabase Integration JWT Template",
|
||||
"$description": "This template is designed for Supabase integration. Name it 'supabase' in Clerk Dashboard. Use with getToken({ template: 'supabase' }) to authenticate Supabase client.",
|
||||
"$usage": "const token = await getToken({ template: 'supabase' }); const supabase = createClient(url, key, { global: { headers: { Authorization: `Bearer ${token}` } } });",
|
||||
|
||||
"aud": "authenticated",
|
||||
"email": "{{user.primary_email_address}}",
|
||||
"app_metadata": {
|
||||
"provider": "clerk",
|
||||
"providers": ["clerk"]
|
||||
},
|
||||
"user_metadata": {
|
||||
"full_name": "{{user.full_name || user.first_name || 'User'}}",
|
||||
"avatar_url": "{{user.image_url}}",
|
||||
"email": "{{user.primary_email_address}}"
|
||||
}
|
||||
}
|
||||
101
templates/nextjs/app-layout.tsx
Normal file
101
templates/nextjs/app-layout.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Next.js App Router Layout with Clerk
|
||||
*
|
||||
* Place this in app/layout.tsx
|
||||
*
|
||||
* Dependencies:
|
||||
* - @clerk/nextjs@^6.33.3
|
||||
*/
|
||||
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
import { Inter } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: 'My App',
|
||||
description: 'Authenticated with Clerk',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<html lang="en">
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* With Dark Mode Support (using next-themes):
|
||||
*
|
||||
* 1. Install: npm install next-themes
|
||||
* 2. Use this pattern:
|
||||
*/
|
||||
/*
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* With Clerk Appearance Customization:
|
||||
*/
|
||||
/*
|
||||
import { ClerkProvider } from '@clerk/nextjs'
|
||||
import { dark } from '@clerk/themes'
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<ClerkProvider
|
||||
appearance={{
|
||||
baseTheme: dark,
|
||||
variables: {
|
||||
colorPrimary: '#3b82f6',
|
||||
colorBackground: '#0f172a',
|
||||
},
|
||||
elements: {
|
||||
formButtonPrimary: 'bg-blue-500 hover:bg-blue-600',
|
||||
card: 'shadow-xl',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
)
|
||||
}
|
||||
*/
|
||||
143
templates/nextjs/middleware.ts
Normal file
143
templates/nextjs/middleware.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Next.js Middleware with Clerk Authentication
|
||||
*
|
||||
* This middleware protects routes using Clerk's clerkMiddleware.
|
||||
* Place this file in the root of your Next.js project.
|
||||
*
|
||||
* Dependencies:
|
||||
* - @clerk/nextjs@^6.33.3
|
||||
*
|
||||
* CRITICAL (v6 Breaking Change):
|
||||
* - auth.protect() is now async - must use await
|
||||
* - Source: https://clerk.com/changelog/2024-10-22-clerk-nextjs-v6
|
||||
*/
|
||||
|
||||
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
||||
|
||||
/**
|
||||
* Define public routes (routes that don't require authentication)
|
||||
*
|
||||
* Glob patterns supported:
|
||||
* - '/path' - exact match
|
||||
* - '/path(.*)' - path and all sub-paths
|
||||
* - '/api/public/*' - wildcard
|
||||
*/
|
||||
const isPublicRoute = createRouteMatcher([
|
||||
'/', // Homepage
|
||||
'/sign-in(.*)', // Sign-in page and sub-paths
|
||||
'/sign-up(.*)', // Sign-up page and sub-paths
|
||||
'/api/public(.*)', // Public API routes
|
||||
'/api/webhooks(.*)', // Webhook endpoints
|
||||
'/about', // Static pages
|
||||
'/pricing',
|
||||
'/contact',
|
||||
])
|
||||
|
||||
/**
|
||||
* Alternative: Define protected routes instead
|
||||
*
|
||||
* Uncomment this pattern if you prefer to explicitly protect
|
||||
* specific routes rather than inverting the logic:
|
||||
*/
|
||||
/*
|
||||
const isProtectedRoute = createRouteMatcher([
|
||||
'/dashboard(.*)',
|
||||
'/profile(.*)',
|
||||
'/admin(.*)',
|
||||
'/api/private(.*)',
|
||||
])
|
||||
|
||||
export default clerkMiddleware(async (auth, request) => {
|
||||
if (isProtectedRoute(request)) {
|
||||
await auth.protect()
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default Pattern: Protect all routes except public ones
|
||||
*
|
||||
* CRITICAL:
|
||||
* - auth.protect() MUST be awaited (async in v6)
|
||||
* - Without await, route protection will not work
|
||||
*/
|
||||
export default clerkMiddleware(async (auth, request) => {
|
||||
if (!isPublicRoute(request)) {
|
||||
await auth.protect()
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Matcher Configuration
|
||||
*
|
||||
* Defines which paths run middleware.
|
||||
* This is the recommended configuration from Clerk.
|
||||
*/
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Skip Next.js internals and static files
|
||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||
|
||||
// Always run for API routes
|
||||
'/(api|trpc)(.*)',
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced: Role-Based Protection
|
||||
*
|
||||
* Protect routes based on user role or organization membership:
|
||||
*/
|
||||
/*
|
||||
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
||||
|
||||
const isAdminRoute = createRouteMatcher(['/admin(.*)'])
|
||||
const isOrgRoute = createRouteMatcher(['/org(.*)'])
|
||||
|
||||
export default clerkMiddleware(async (auth, request) => {
|
||||
// Admin routes require 'admin' role
|
||||
if (isAdminRoute(request)) {
|
||||
await auth.protect((has) => {
|
||||
return has({ role: 'admin' })
|
||||
})
|
||||
}
|
||||
|
||||
// Organization routes require organization membership
|
||||
if (isOrgRoute(request)) {
|
||||
await auth.protect((has) => {
|
||||
return has({ permission: 'org:member' })
|
||||
})
|
||||
}
|
||||
|
||||
// All other routes use default protection
|
||||
if (!isPublicRoute(request)) {
|
||||
await auth.protect()
|
||||
}
|
||||
})
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||
'/(api|trpc)(.*)',
|
||||
],
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Troubleshooting:
|
||||
*
|
||||
* 1. Routes not protected?
|
||||
* - Ensure auth.protect() is awaited
|
||||
* - Check matcher configuration includes your routes
|
||||
* - Verify middleware.ts is in project root
|
||||
*
|
||||
* 2. Infinite redirects?
|
||||
* - Ensure sign-in/sign-up routes are in isPublicRoute
|
||||
* - Check NEXT_PUBLIC_CLERK_SIGN_IN_URL in .env.local
|
||||
*
|
||||
* 3. API routes returning HTML?
|
||||
* - Verify '/(api|trpc)(.*)' is in matcher
|
||||
* - Check API routes are not in isPublicRoute if protected
|
||||
*
|
||||
* Official Docs: https://clerk.com/docs/reference/nextjs/clerk-middleware
|
||||
*/
|
||||
114
templates/nextjs/server-component-example.tsx
Normal file
114
templates/nextjs/server-component-example.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Server Component with Clerk Auth
|
||||
*
|
||||
* Demonstrates using auth() and currentUser() in Server Components
|
||||
*
|
||||
* CRITICAL (v6): auth() is now async - must use await
|
||||
*/
|
||||
|
||||
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default async function DashboardPage() {
|
||||
/**
|
||||
* Option 1: Lightweight auth check
|
||||
*
|
||||
* Use auth() when you only need userId/sessionId
|
||||
* This is faster than currentUser()
|
||||
*/
|
||||
const { userId, sessionId } = await auth()
|
||||
|
||||
// Redirect if not authenticated (shouldn't happen if middleware configured)
|
||||
if (!userId) {
|
||||
redirect('/sign-in')
|
||||
}
|
||||
|
||||
/**
|
||||
* Option 2: Full user object
|
||||
*
|
||||
* Use currentUser() when you need full user data
|
||||
* Heavier than auth(), so use sparingly
|
||||
*/
|
||||
const user = await currentUser()
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
|
||||
<div className="mt-4 space-y-2">
|
||||
<p>
|
||||
<strong>User ID:</strong> {userId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Session ID:</strong> {sessionId}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Email:</strong>{' '}
|
||||
{user?.primaryEmailAddress?.emailAddress}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Name:</strong> {user?.firstName} {user?.lastName}
|
||||
</p>
|
||||
|
||||
{/* Access public metadata */}
|
||||
{user?.publicMetadata && (
|
||||
<div>
|
||||
<strong>Role:</strong>{' '}
|
||||
{(user.publicMetadata as any).role || 'user'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* API Route Example (app/api/user/route.ts)
|
||||
*/
|
||||
/*
|
||||
import { auth, currentUser } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function GET() {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const user = await currentUser()
|
||||
|
||||
return NextResponse.json({
|
||||
userId,
|
||||
email: user?.primaryEmailAddress?.emailAddress,
|
||||
name: `${user?.firstName} ${user?.lastName}`,
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
/**
|
||||
* Protected API Route with POST (app/api/items/route.ts)
|
||||
*/
|
||||
/*
|
||||
import { auth } from '@clerk/nextjs/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { userId } = await auth()
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const body = await request.json()
|
||||
|
||||
// Validate and process
|
||||
// Example: save to database with userId
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
itemId: crypto.randomUUID(),
|
||||
userId,
|
||||
}, { status: 201 })
|
||||
}
|
||||
*/
|
||||
215
templates/react/App.tsx
Normal file
215
templates/react/App.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* React App Component with Clerk Hooks
|
||||
*
|
||||
* Demonstrates:
|
||||
* - useUser() for user data
|
||||
* - useAuth() for session tokens
|
||||
* - useClerk() for auth methods
|
||||
* - Proper loading state handling
|
||||
*/
|
||||
|
||||
import { useUser, useAuth, useClerk, SignInButton, UserButton } from '@clerk/clerk-react'
|
||||
|
||||
function App() {
|
||||
// Get user object (includes email, metadata, etc.)
|
||||
const { isLoaded, isSignedIn, user } = useUser()
|
||||
|
||||
// Get auth state and session methods
|
||||
const { userId, getToken } = useAuth()
|
||||
|
||||
// Get Clerk instance for advanced operations
|
||||
const { openSignIn, signOut } = useClerk()
|
||||
|
||||
/**
|
||||
* CRITICAL: Always check isLoaded before rendering
|
||||
*
|
||||
* Why: Prevents flash of wrong content while Clerk initializes
|
||||
* Source: https://clerk.com/docs/references/react/use-user
|
||||
*/
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-lg">Loading...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthenticated View
|
||||
*/
|
||||
if (!isSignedIn) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen gap-4">
|
||||
<h1 className="text-4xl font-bold">Welcome</h1>
|
||||
<p className="text-gray-600">Sign in to continue</p>
|
||||
|
||||
{/* Option 1: Clerk's pre-built button */}
|
||||
<SignInButton mode="modal">
|
||||
<button className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
|
||||
Sign In
|
||||
</button>
|
||||
</SignInButton>
|
||||
|
||||
{/* Option 2: Custom button with openSignIn() */}
|
||||
{/* <button
|
||||
onClick={() => openSignIn()}
|
||||
className="px-6 py-2 bg-blue-500 text-white rounded-lg"
|
||||
>
|
||||
Sign In
|
||||
</button> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated View
|
||||
*/
|
||||
return (
|
||||
<div className="container mx-auto p-8">
|
||||
<header className="flex items-center justify-between mb-8">
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
|
||||
{/* Clerk's pre-built user button (profile + sign out) */}
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
</header>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">User Information</h2>
|
||||
|
||||
<dl className="space-y-2">
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">User ID</dt>
|
||||
<dd className="text-gray-900">{userId}</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Email</dt>
|
||||
<dd className="text-gray-900">
|
||||
{user.primaryEmailAddress?.emailAddress}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Name</dt>
|
||||
<dd className="text-gray-900">
|
||||
{user.firstName} {user.lastName}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
{/* Access public metadata */}
|
||||
{user.publicMetadata && Object.keys(user.publicMetadata).length > 0 && (
|
||||
<div>
|
||||
<dt className="font-medium text-gray-700">Metadata</dt>
|
||||
<dd className="text-gray-900">
|
||||
<pre className="text-sm bg-gray-100 p-2 rounded">
|
||||
{JSON.stringify(user.publicMetadata, null, 2)}
|
||||
</pre>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{/* Example: Call protected API */}
|
||||
<ProtectedAPIExample getToken={getToken} />
|
||||
|
||||
{/* Custom sign out button */}
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Example Component: Calling Protected API
|
||||
*/
|
||||
function ProtectedAPIExample({ getToken }: { getToken: () => Promise<string | null> }) {
|
||||
const [data, setData] = React.useState<any>(null)
|
||||
const [loading, setLoading] = React.useState(false)
|
||||
const [error, setError] = React.useState<string | null>(null)
|
||||
|
||||
const fetchProtectedData = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
// Get fresh session token (auto-refreshes)
|
||||
const token = await getToken()
|
||||
|
||||
if (!token) {
|
||||
throw new Error('No session token available')
|
||||
}
|
||||
|
||||
// Call your API with Authorization header
|
||||
const response = await fetch('https://your-worker.workers.dev/api/protected', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`API error: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
setData(result)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h2 className="text-xl font-semibold mb-4">Protected API Call</h2>
|
||||
|
||||
<button
|
||||
onClick={fetchProtectedData}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Loading...' : 'Fetch Protected Data'}
|
||||
</button>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-4 bg-red-50 text-red-700 rounded">
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data && (
|
||||
<div className="mt-4">
|
||||
<pre className="text-sm bg-gray-100 p-4 rounded overflow-auto">
|
||||
{JSON.stringify(data, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
/**
|
||||
* Troubleshooting:
|
||||
*
|
||||
* 1. "Missing Publishable Key" error?
|
||||
* - Check .env.local has VITE_CLERK_PUBLISHABLE_KEY
|
||||
* - Restart dev server after adding env var
|
||||
*
|
||||
* 2. Flash of unauthenticated content?
|
||||
* - Always check isLoaded before rendering
|
||||
* - Show loading state while isLoaded is false
|
||||
*
|
||||
* 3. Token not working with API?
|
||||
* - Ensure getToken() is called fresh (don't cache)
|
||||
* - Check Authorization header format: "Bearer <token>"
|
||||
* - Verify API is using @clerk/backend to verify token
|
||||
*/
|
||||
66
templates/react/main.tsx
Normal file
66
templates/react/main.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* React + Vite Entry Point with Clerk
|
||||
*
|
||||
* Place this in src/main.tsx
|
||||
*
|
||||
* Dependencies:
|
||||
* - @clerk/clerk-react@^5.51.0
|
||||
*/
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { ClerkProvider } from '@clerk/clerk-react'
|
||||
import App from './App.tsx'
|
||||
import './index.css'
|
||||
|
||||
// Get publishable key from environment
|
||||
// CRITICAL: Must use VITE_ prefix for Vite to expose to client
|
||||
const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
|
||||
|
||||
if (!PUBLISHABLE_KEY) {
|
||||
throw new Error('Missing VITE_CLERK_PUBLISHABLE_KEY in .env.local')
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
|
||||
<App />
|
||||
</ClerkProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
|
||||
/**
|
||||
* With Dark Mode Support (using custom theme):
|
||||
*/
|
||||
/*
|
||||
import { ClerkProvider } from '@clerk/clerk-react'
|
||||
import { dark } from '@clerk/themes'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<ClerkProvider
|
||||
publishableKey={PUBLISHABLE_KEY}
|
||||
appearance={{
|
||||
baseTheme: dark,
|
||||
variables: {
|
||||
colorPrimary: '#3b82f6',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</ClerkProvider>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Environment Variables:
|
||||
*
|
||||
* Create .env.local with:
|
||||
* VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
|
||||
*
|
||||
* CRITICAL:
|
||||
* - Must use VITE_ prefix (Vite requirement)
|
||||
* - Never commit .env.local to version control
|
||||
* - Use different keys for development and production
|
||||
*/
|
||||
125
templates/typescript/custom-jwt-types.d.ts
vendored
Normal file
125
templates/typescript/custom-jwt-types.d.ts
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Custom JWT Session Claims Type Definitions
|
||||
*
|
||||
* This file provides TypeScript type safety for custom JWT claims in Clerk.
|
||||
* Place this in your project's types/ directory (e.g., types/globals.d.ts).
|
||||
*
|
||||
* After adding this, sessionClaims will have auto-complete and type checking
|
||||
* for your custom claims.
|
||||
*
|
||||
* Usage:
|
||||
* ```typescript
|
||||
* import { auth } from '@clerk/nextjs/server'
|
||||
*
|
||||
* const { sessionClaims } = await auth()
|
||||
* const role = sessionClaims?.metadata?.role // Type: 'admin' | 'moderator' | 'user' | undefined
|
||||
* ```
|
||||
*/
|
||||
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* Extend Clerk's CustomJwtSessionClaims interface with your custom claims.
|
||||
*
|
||||
* IMPORTANT: The structure must match your JWT template exactly.
|
||||
*/
|
||||
interface CustomJwtSessionClaims {
|
||||
/**
|
||||
* Custom metadata claims
|
||||
*/
|
||||
metadata: {
|
||||
/**
|
||||
* User's role in the application
|
||||
* Maps to: {{user.public_metadata.role}}
|
||||
*/
|
||||
role?: 'admin' | 'moderator' | 'user'
|
||||
|
||||
/**
|
||||
* Whether user has completed onboarding
|
||||
* Maps to: {{user.public_metadata.onboardingComplete}}
|
||||
*/
|
||||
onboardingComplete?: boolean
|
||||
|
||||
/**
|
||||
* User's department
|
||||
* Maps to: {{user.public_metadata.department}}
|
||||
*/
|
||||
department?: string
|
||||
|
||||
/**
|
||||
* User's permissions array
|
||||
* Maps to: {{user.public_metadata.permissions}}
|
||||
*/
|
||||
permissions?: string[]
|
||||
|
||||
/**
|
||||
* Organization ID for multi-tenant apps
|
||||
* Maps to: {{user.public_metadata.org_id}}
|
||||
*/
|
||||
organizationId?: string
|
||||
|
||||
/**
|
||||
* Organization slug for multi-tenant apps
|
||||
* Maps to: {{user.public_metadata.org_slug}}
|
||||
*/
|
||||
organizationSlug?: string
|
||||
|
||||
/**
|
||||
* User's role in organization
|
||||
* Maps to: {{user.public_metadata.org_role}}
|
||||
*/
|
||||
organizationRole?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* User's email address (if included in template)
|
||||
* Maps to: {{user.primary_email_address}}
|
||||
*/
|
||||
email?: string
|
||||
|
||||
/**
|
||||
* User's full name (if included in template)
|
||||
* Maps to: {{user.full_name}}
|
||||
*/
|
||||
full_name?: string
|
||||
|
||||
/**
|
||||
* User ID (if included in template)
|
||||
* Maps to: {{user.id}}
|
||||
* Note: Also available as 'sub' in default claims
|
||||
*/
|
||||
user_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Example Usage in Next.js Server Component:
|
||||
*
|
||||
* ```typescript
|
||||
* import { auth } from '@clerk/nextjs/server'
|
||||
*
|
||||
* export default async function AdminPage() {
|
||||
* const { sessionClaims } = await auth()
|
||||
*
|
||||
* // TypeScript knows about these properties now
|
||||
* if (sessionClaims?.metadata?.role !== 'admin') {
|
||||
* return <div>Unauthorized</div>
|
||||
* }
|
||||
*
|
||||
* return <div>Admin Dashboard</div>
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Example Usage in Cloudflare Workers:
|
||||
*
|
||||
* ```typescript
|
||||
* import { verifyToken } from '@clerk/backend'
|
||||
*
|
||||
* const { data } = await verifyToken(token, { secretKey })
|
||||
*
|
||||
* // Access custom claims with type safety
|
||||
* const role = data.metadata?.role
|
||||
* const isAdmin = role === 'admin'
|
||||
* ```
|
||||
*/
|
||||
42
templates/vite/package.json
Normal file
42
templates/vite/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$comment": "Vite + Clerk: package.json with increased header size limit",
|
||||
"$description": "This template shows how to configure Vite dev server to handle Clerk's authentication handshake tokens, which can exceed the default 8KB Node.js header limit when using custom JWT claims.",
|
||||
|
||||
"name": "vite-clerk-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
"scripts": {
|
||||
"dev": "NODE_OPTIONS='--max-http-header-size=32768' vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"@clerk/clerk-react": "^5.51.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^6.0.0"
|
||||
},
|
||||
|
||||
"$notes": [
|
||||
"The key change is in the 'dev' script:",
|
||||
"NODE_OPTIONS='--max-http-header-size=32768' increases the limit from 8KB to 32KB",
|
||||
"",
|
||||
"For Windows PowerShell, use:",
|
||||
"\"dev\": \"cross-env NODE_OPTIONS=--max-http-header-size=32768 vite\"",
|
||||
"And install: npm install -D cross-env",
|
||||
"",
|
||||
"This prevents '431 Request Header Fields Too Large' errors when:",
|
||||
"- Testing Clerk authentication in development mode",
|
||||
"- Using custom JWT claims that increase token size",
|
||||
"- Clerk's __clerk_handshake parameter exceeds header limit",
|
||||
"",
|
||||
"Production deployments (Cloudflare, Vercel, Netlify) don't need this fix."
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user