Files
gh-jezweb-claude-skills-ski…/templates/turnstile-hono-route.ts
2025-11-30 08:24:31 +08:00

249 lines
6.1 KiB
TypeScript

/**
* Turnstile + Hono Route Handlers
*
* Complete examples for integrating Turnstile validation
* with Hono API routes in Cloudflare Workers
*/
import { Hono } from 'hono'
import { validateTurnstile, type TurnstileResponse } from './turnstile-server-validation'
/**
* Environment Bindings
*/
type Bindings = {
TURNSTILE_SECRET_KEY: string
TURNSTILE_SITE_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
/**
* Example 1: Simple Contact Form
*/
app.post('/api/contact', async (c) => {
try {
const body = await c.req.formData()
const token = body.get('cf-turnstile-response')
if (!token) {
return c.text('Missing Turnstile token', 400)
}
// Validate token
const verifyFormData = new FormData()
verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token.toString())
verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '')
const verifyResult = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
body: verifyFormData,
}
)
const outcome = await verifyResult.json<{ success: boolean }>()
if (!outcome.success) {
return c.text('Invalid Turnstile token', 401)
}
// Process contact form
const email = body.get('email')?.toString()
const message = body.get('message')?.toString()
console.log('Contact form submitted:', { email, message })
// Your business logic here (send email, save to DB, etc.)
return c.json({ message: 'Contact form submitted successfully' })
} catch (error) {
console.error('Contact form error:', error)
return c.text('Internal server error', 500)
}
})
/**
* Example 2: Login with Turnstile
*/
app.post('/api/auth/login', async (c) => {
try {
const { username, password, 'cf-turnstile-response': token } = await c.req.json()
// Validate Turnstile token first
const result = await validateTurnstile(
token,
c.env.TURNSTILE_SECRET_KEY,
{
remoteip: c.req.header('CF-Connecting-IP'),
expectedAction: 'login',
expectedHostname: new URL(c.req.url).hostname,
}
)
if (!result.success) {
return c.json(
{
error: 'Invalid Turnstile token',
codes: result['error-codes'],
},
401
)
}
// Validate credentials (example - use proper auth in production)
if (!username || !password) {
return c.json({ error: 'Missing credentials' }, 400)
}
// Check credentials against database
// const user = await db.query('SELECT * FROM users WHERE username = ?', [username])
// Create session token
// const sessionToken = await createSession(user.id)
return c.json({
message: 'Login successful',
// token: sessionToken,
})
} catch (error) {
console.error('Login error:', error)
return c.json({ error: 'Login failed' }, 500)
}
})
/**
* Example 3: Signup with Turnstile + Rate Limiting
*/
app.post('/api/auth/signup', async (c) => {
try {
const { email, password, 'cf-turnstile-response': token } = await c.req.json()
// Validate Turnstile
const result = await validateTurnstile(
token,
c.env.TURNSTILE_SECRET_KEY,
{
remoteip: c.req.header('CF-Connecting-IP'),
expectedAction: 'signup',
}
)
if (!result.success) {
return c.json({ error: 'Bot detection failed' }, 401)
}
// Validate input
if (!email || !password) {
return c.json({ error: 'Missing required fields' }, 400)
}
// Check if user exists
// const existingUser = await db.query('SELECT id FROM users WHERE email = ?', [email])
// if (existingUser) {
// return c.json({ error: 'User already exists' }, 409)
// }
// Create user
// const hashedPassword = await hashPassword(password)
// await db.query('INSERT INTO users (email, password) VALUES (?, ?)', [email, hashedPassword])
return c.json({
message: 'Signup successful',
})
} catch (error) {
console.error('Signup error:', error)
return c.json({ error: 'Signup failed' }, 500)
}
})
/**
* Example 4: Middleware for Turnstile Validation
*/
async function turnstileMiddleware(c: any, next: () => Promise<void>) {
const contentType = c.req.header('Content-Type')
let token: string | null = null
// Get token from FormData or JSON
if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
const formData = await c.req.formData()
token = formData.get('cf-turnstile-response')?.toString() || null
} else if (contentType?.includes('application/json')) {
const body = await c.req.json()
token = body['cf-turnstile-response'] || null
}
if (!token) {
return c.json({ error: 'Missing Turnstile token' }, 400)
}
// Validate token
const result = await validateTurnstile(
token,
c.env.TURNSTILE_SECRET_KEY,
{
remoteip: c.req.header('CF-Connecting-IP'),
}
)
if (!result.success) {
return c.json({
error: 'Turnstile validation failed',
codes: result['error-codes'],
}, 401)
}
// Store result in context for route handler
c.set('turnstileResult', result)
await next()
}
/**
* Example 5: Using Middleware
*/
app.post('/api/protected/action', turnstileMiddleware, async (c) => {
const turnstileResult = c.get('turnstileResult') as TurnstileResponse
console.log('Turnstile validated:', turnstileResult)
// Your protected action here
return c.json({ message: 'Action completed successfully' })
})
/**
* Example 6: Get Sitekey Endpoint (for frontend)
*/
app.get('/api/turnstile/sitekey', (c) => {
return c.json({
sitekey: c.env.TURNSTILE_SITE_KEY,
})
})
/**
* Example 7: Health Check (without Turnstile)
*/
app.get('/health', (c) => {
return c.json({ status: 'ok' })
})
/**
* Example 8: CORS for Turnstile
*/
import { cors } from 'hono/cors'
app.use('/api/*', cors({
origin: ['https://yourdomain.com', 'http://localhost:5173'],
allowMethods: ['POST', 'GET', 'OPTIONS'],
allowHeaders: ['Content-Type'],
credentials: true,
}))
/**
* Export
*/
export default app