@businessflow/reviews 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +0 -99
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +86 -1
- package/dist/server/index.d.ts +86 -1
- package/dist/server/index.js +223 -99
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +219 -99
- package/dist/server/index.mjs.map +1 -1
- package/package.json +6 -6
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts","../../src/server/handler.ts","../../src/server/recaptcha.ts"],"sourcesContent":["// Server-side utilities for NextJS API routes\nexport { \n createReviewHandler, \n createReviewFetchHandler, \n createReviewSubmitHandler,\n createCorsHeaders, \n handleOptions \n} from './handler';\nexport { verifyRecaptcha, getRecaptchaErrorMessage } from './recaptcha';\nexport type { RecaptchaConfig, RecaptchaVerificationResult } from './recaptcha';\nexport * from '../types';","import { NextRequest, NextResponse } from 'next/server';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\nimport { verifyRecaptcha, getRecaptchaErrorMessage } from './recaptcha';\n\n/**\n * Create a generic NextJS API route handler for review submission\n */\nexport function createReviewSubmitHandler(config: ReviewHandlerConfig) {\n return async function reviewSubmitHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow POST requests\n if (request.method !== 'POST') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'POST' } }\n );\n }\n\n try {\n // Parse request body\n let body: ReviewFormData;\n try {\n body = await request.json();\n } catch (parseError) {\n return NextResponse.json(\n { error: 'Invalid JSON in request body' },\n { status: 400 }\n );\n }\n\n // Rate limiting (if configured)\n if (config.rateLimiter) {\n const allowed = await config.rateLimiter(request);\n if (!allowed) {\n return NextResponse.json(\n { error: 'Too many requests. Please try again later.' },\n { status: 429 }\n );\n }\n }\n\n // Validate required fields (basic validation)\n if (!body.reviewerName || typeof body.reviewerName !== 'string' || body.reviewerName.trim().length === 0) {\n return NextResponse.json(\n { error: 'Reviewer name is required' },\n { status: 400 }\n );\n }\n\n if (!body.reviewerEmail || typeof body.reviewerEmail !== 'string' || body.reviewerEmail.trim().length === 0) {\n return NextResponse.json(\n { error: 'Email is required' },\n { status: 400 }\n );\n }\n\n // Email format validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(body.reviewerEmail.trim())) {\n return NextResponse.json(\n { error: 'Please enter a valid email address' },\n { status: 400 }\n );\n }\n\n // Rating validation\n if (typeof body.rating !== 'number' || body.rating < 1 || body.rating > 5 || !Number.isInteger(body.rating)) {\n return NextResponse.json(\n { error: 'Rating must be between 1 and 5 stars' },\n { status: 400 }\n );\n }\n\n // Custom validation (if configured)\n if (config.validation) {\n for (const [field, rules] of Object.entries(config.validation)) {\n const value = (body as any)[field];\n \n for (const rule of rules) {\n let isValid = true;\n let errorMessage = rule.message || 'Validation failed';\n\n switch (rule.type) {\n case 'required':\n isValid = value != null && value !== '' && \n (typeof value !== 'string' || value.trim() !== '');\n break;\n \n case 'email':\n if (value) {\n isValid = emailRegex.test(String(value).trim());\n }\n break;\n \n case 'rating':\n if (value !== undefined) {\n isValid = typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 5;\n }\n break;\n \n case 'minLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length >= rule.value;\n }\n break;\n \n case 'maxLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length <= rule.value;\n }\n break;\n \n case 'pattern':\n if (value && typeof rule.value === 'string') {\n const regex = new RegExp(rule.value);\n isValid = regex.test(String(value));\n }\n break;\n \n case 'custom':\n if (rule.validator) {\n const result = rule.validator(value);\n isValid = result === true;\n if (typeof result === 'string') {\n errorMessage = result;\n }\n }\n break;\n }\n\n if (!isValid) {\n return NextResponse.json(\n { error: errorMessage },\n { status: 400 }\n );\n }\n }\n }\n }\n\n // reCAPTCHA verification (if configured)\n if (config.recaptcha && (body as any).RecaptchaToken) {\n const recaptchaResult = await verifyRecaptcha((body as any).RecaptchaToken, config.recaptcha);\n \n if (!recaptchaResult.success) {\n const errorMessage = getRecaptchaErrorMessage(recaptchaResult.errorCodes || []);\n return NextResponse.json(\n { error: `reCAPTCHA verification failed: ${errorMessage}` },\n { status: 400 }\n );\n }\n }\n\n // Call the user-provided submit function\n let response: ReviewApiResponse;\n try {\n if (!config.onSubmit) {\n return NextResponse.json(\n { error: 'Review submission not configured' },\n { status: 500 }\n );\n }\n\n response = await config.onSubmit(body);\n } catch (submitError) {\n console.error('Review submission error:', submitError);\n \n // Call error callback if provided\n if (config.onError) {\n await config.onError(body, submitError instanceof Error ? submitError : new Error(String(submitError)));\n }\n\n return NextResponse.json(\n { error: 'Failed to submit review. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from submit function\n if (!response || typeof response !== 'object') {\n console.error('Invalid response from submit function:', response);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Handle unsuccessful submission\n if (!response.success) {\n const statusCode = response.message?.includes('validation') ? 400 : 500;\n return NextResponse.json(\n { \n error: response.message || 'Failed to submit review',\n data: response.data \n },\n { status: statusCode }\n );\n }\n\n // Call success callback if provided\n if (config.onSuccess) {\n try {\n await config.onSuccess(body, response);\n } catch (callbackError) {\n console.error('Success callback error:', callbackError);\n // Don't fail the request if callback fails\n }\n }\n\n // Return successful response\n return NextResponse.json({\n success: true,\n message: response.message || 'Review submitted successfully',\n reviewId: response.reviewId,\n status: response.status,\n data: response.data\n });\n\n } catch (error) {\n console.error('Unexpected error in review submit handler:', error);\n \n // Try to call error callback\n if (config.onError) {\n try {\n await config.onError({} as ReviewFormData, error instanceof Error ? error : new Error(String(error)));\n } catch (callbackError) {\n console.error('Error callback failed:', callbackError);\n }\n }\n\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Create a generic NextJS API route handler for fetching reviews\n */\nexport function createReviewFetchHandler(config: ReviewHandlerConfig) {\n return async function reviewFetchHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow GET requests\n if (request.method !== 'GET') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET' } }\n );\n }\n\n try {\n // Parse query parameters\n const searchParams = request.nextUrl.searchParams;\n const params = {\n limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined,\n offset: searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : undefined,\n featured: searchParams.get('featured') === 'true' ? true : undefined,\n minRating: searchParams.get('minRating') ? parseInt(searchParams.get('minRating')!) : undefined,\n sortBy: searchParams.get('sortBy') as 'date' | 'rating' | 'name' | undefined,\n sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc' | undefined,\n };\n\n // Call the user-provided fetch function\n let reviews: Review[];\n try {\n if (!config.onFetch) {\n return NextResponse.json(\n { error: 'Review fetching not configured' },\n { status: 500 }\n );\n }\n\n reviews = await config.onFetch(params);\n } catch (fetchError) {\n console.error('Review fetch error:', fetchError);\n \n return NextResponse.json(\n { error: 'Failed to fetch reviews. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from fetch function\n if (!Array.isArray(reviews)) {\n console.error('Invalid response from fetch function, expected array:', reviews);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Return successful response\n return NextResponse.json(reviews);\n\n } catch (error) {\n console.error('Unexpected error in review fetch handler:', error);\n \n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Combined handler that supports both GET (fetch) and POST (submit)\n */\nexport function createReviewHandler(config: ReviewHandlerConfig) {\n const fetchHandler = createReviewFetchHandler(config);\n const submitHandler = createReviewSubmitHandler(config);\n\n return async function combinedHandler(request: NextRequest): Promise<NextResponse> {\n if (request.method === 'GET') {\n return fetchHandler(request);\n } else if (request.method === 'POST') {\n return submitHandler(request);\n } else {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET, POST' } }\n );\n }\n };\n}\n\n/**\n * Helper function to create CORS headers for the response\n */\nexport function createCorsHeaders(allowedOrigins?: string[]): HeadersInit {\n const headers: HeadersInit = {\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n };\n\n if (allowedOrigins && allowedOrigins.length > 0) {\n headers['Access-Control-Allow-Origin'] = allowedOrigins.join(', ');\n } else {\n headers['Access-Control-Allow-Origin'] = '*';\n }\n\n return headers;\n}\n\n/**\n * Helper to handle OPTIONS requests for CORS\n */\nexport function handleOptions(allowedOrigins?: string[]): NextResponse {\n return new NextResponse(null, {\n status: 200,\n headers: createCorsHeaders(allowedOrigins)\n });\n}","/**\n * Server-side reCAPTCHA verification utility\n */\n\nexport interface RecaptchaConfig {\n secretKey: string;\n minimumScore?: number; // For reCAPTCHA v3, minimum score (0.0 to 1.0)\n timeoutMs?: number; // Request timeout in milliseconds\n}\n\nexport interface RecaptchaVerificationResult {\n success: boolean;\n score?: number; // reCAPTCHA v3 score\n action?: string; // reCAPTCHA v3 action\n challengeTimestamp?: string;\n hostname?: string;\n errorCodes?: string[];\n}\n\n/**\n * Verify reCAPTCHA token with Google's API\n */\nexport async function verifyRecaptcha(\n token: string,\n config: RecaptchaConfig\n): Promise<RecaptchaVerificationResult> {\n if (!config.secretKey) {\n console.warn('RECAPTCHA_SECRET_KEY not configured, skipping verification');\n return { success: true };\n }\n\n if (!token || typeof token !== 'string') {\n return {\n success: false,\n errorCodes: ['missing-input-response']\n };\n }\n\n const timeout = config.timeoutMs || 10000; // 10 second default timeout\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n secret: config.secretKey,\n response: token\n }),\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.error('reCAPTCHA API returned non-OK status:', response.status);\n return {\n success: false,\n errorCodes: ['recaptcha-api-error']\n };\n }\n\n const data = await response.json();\n \n // Check minimum score for v3 (if configured)\n if (config.minimumScore !== undefined && data.score !== undefined) {\n if (data.score < config.minimumScore) {\n return {\n success: false,\n score: data.score,\n errorCodes: ['score-threshold-not-met']\n };\n }\n }\n\n return {\n success: data.success === true,\n score: data.score,\n action: data.action,\n challengeTimestamp: data.challenge_ts,\n hostname: data.hostname,\n errorCodes: data['error-codes'] || []\n };\n\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n console.error('reCAPTCHA verification timeout');\n return {\n success: false,\n errorCodes: ['timeout-or-duplicate']\n };\n }\n\n console.error('reCAPTCHA verification error:', error);\n return {\n success: false,\n errorCodes: ['network-error']\n };\n }\n}\n\n/**\n * Get human-readable error message for reCAPTCHA error codes\n */\nexport function getRecaptchaErrorMessage(errorCodes: string[]): string {\n const errorMessages: { [key: string]: string } = {\n 'missing-input-secret': 'reCAPTCHA secret key is missing',\n 'invalid-input-secret': 'reCAPTCHA secret key is invalid',\n 'missing-input-response': 'reCAPTCHA token is missing',\n 'invalid-input-response': 'reCAPTCHA token is invalid or malformed',\n 'bad-request': 'The request is invalid or malformed',\n 'timeout-or-duplicate': 'reCAPTCHA verification timed out or token was already used',\n 'score-threshold-not-met': 'reCAPTCHA score is below the required threshold',\n 'recaptcha-api-error': 'reCAPTCHA service is unavailable',\n 'network-error': 'Network error during reCAPTCHA verification'\n };\n\n if (!errorCodes || errorCodes.length === 0) {\n return 'reCAPTCHA verification failed';\n }\n\n const knownErrors = errorCodes\n .filter(code => errorMessages[code])\n .map(code => errorMessages[code]);\n\n return knownErrors.length > 0 \n ? knownErrors.join(', ')\n : 'reCAPTCHA verification failed';\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts","../../src/server/handler.ts","../../src/server/recaptcha.ts","../../src/server/businessflow.ts"],"sourcesContent":["// Server-side utilities for NextJS API routes\nexport {\n createReviewHandler,\n createReviewFetchHandler,\n createReviewSubmitHandler,\n createCorsHeaders,\n handleOptions\n} from './handler';\nexport { verifyRecaptcha, getRecaptchaErrorMessage } from './recaptcha';\nexport {\n createBusinessFlowReviewHandler,\n createBusinessFlowFeaturedHandler,\n createSimpleBusinessFlowReviewHandler,\n createSimpleBusinessFlowFeaturedHandler\n} from './businessflow';\nexport type { RecaptchaConfig, RecaptchaVerificationResult } from './recaptcha';\nexport type { BusinessFlowReviewConfig } from './businessflow';\nexport * from '../types';","import { NextRequest, NextResponse } from 'next/server';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\n\n/**\n * Create a generic NextJS API route handler for review submission\n */\nexport function createReviewSubmitHandler(config: ReviewHandlerConfig) {\n return async function reviewSubmitHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow POST requests\n if (request.method !== 'POST') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'POST' } }\n );\n }\n\n try {\n // Parse request body\n let body: ReviewFormData;\n try {\n body = await request.json();\n } catch (parseError) {\n return NextResponse.json(\n { error: 'Invalid JSON in request body' },\n { status: 400 }\n );\n }\n\n // Rate limiting (if configured)\n if (config.rateLimiter) {\n const allowed = await config.rateLimiter(request);\n if (!allowed) {\n return NextResponse.json(\n { error: 'Too many requests. Please try again later.' },\n { status: 429 }\n );\n }\n }\n\n // Validate required fields (basic validation)\n if (!body.reviewerName || typeof body.reviewerName !== 'string' || body.reviewerName.trim().length === 0) {\n return NextResponse.json(\n { error: 'Reviewer name is required' },\n { status: 400 }\n );\n }\n\n if (!body.reviewerEmail || typeof body.reviewerEmail !== 'string' || body.reviewerEmail.trim().length === 0) {\n return NextResponse.json(\n { error: 'Email is required' },\n { status: 400 }\n );\n }\n\n // Email format validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(body.reviewerEmail.trim())) {\n return NextResponse.json(\n { error: 'Please enter a valid email address' },\n { status: 400 }\n );\n }\n\n // Rating validation\n if (typeof body.rating !== 'number' || body.rating < 1 || body.rating > 5 || !Number.isInteger(body.rating)) {\n return NextResponse.json(\n { error: 'Rating must be between 1 and 5 stars' },\n { status: 400 }\n );\n }\n\n // Custom validation (if configured)\n if (config.validation) {\n for (const [field, rules] of Object.entries(config.validation)) {\n const value = (body as any)[field];\n \n for (const rule of rules) {\n let isValid = true;\n let errorMessage = rule.message || 'Validation failed';\n\n switch (rule.type) {\n case 'required':\n isValid = value != null && value !== '' && \n (typeof value !== 'string' || value.trim() !== '');\n break;\n \n case 'email':\n if (value) {\n isValid = emailRegex.test(String(value).trim());\n }\n break;\n \n case 'rating':\n if (value !== undefined) {\n isValid = typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 5;\n }\n break;\n \n case 'minLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length >= rule.value;\n }\n break;\n \n case 'maxLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length <= rule.value;\n }\n break;\n \n case 'pattern':\n if (value && typeof rule.value === 'string') {\n const regex = new RegExp(rule.value);\n isValid = regex.test(String(value));\n }\n break;\n \n case 'custom':\n if (rule.validator) {\n const result = rule.validator(value);\n isValid = result === true;\n if (typeof result === 'string') {\n errorMessage = result;\n }\n }\n break;\n }\n\n if (!isValid) {\n return NextResponse.json(\n { error: errorMessage },\n { status: 400 }\n );\n }\n }\n }\n }\n\n // reCAPTCHA verification (if configured)\n // if (config.recaptcha && (body as any).RecaptchaToken) {\n // const recaptchaResult = await verifyRecaptcha((body as any).RecaptchaToken, config.recaptcha);\n \n // if (!recaptchaResult.success) {\n // const errorMessage = getRecaptchaErrorMessage(recaptchaResult.errorCodes || []);\n // return NextResponse.json(\n // { error: `reCAPTCHA verification failed: ${errorMessage}` },\n // { status: 400 }\n // );\n // }\n // }\n\n // Call the user-provided submit function\n let response: ReviewApiResponse;\n try {\n if (!config.onSubmit) {\n return NextResponse.json(\n { error: 'Review submission not configured' },\n { status: 500 }\n );\n }\n\n response = await config.onSubmit(body);\n } catch (submitError) {\n console.error('Review submission error:', submitError);\n \n // Call error callback if provided\n if (config.onError) {\n await config.onError(body, submitError instanceof Error ? submitError : new Error(String(submitError)));\n }\n\n return NextResponse.json(\n { error: 'Failed to submit review. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from submit function\n if (!response || typeof response !== 'object') {\n console.error('Invalid response from submit function:', response);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Handle unsuccessful submission\n if (!response.success) {\n const statusCode = response.message?.includes('validation') ? 400 : 500;\n return NextResponse.json(\n { \n error: response.message || 'Failed to submit review',\n data: response.data \n },\n { status: statusCode }\n );\n }\n\n // Call success callback if provided\n if (config.onSuccess) {\n try {\n await config.onSuccess(body, response);\n } catch (callbackError) {\n console.error('Success callback error:', callbackError);\n // Don't fail the request if callback fails\n }\n }\n\n // Return successful response\n return NextResponse.json({\n success: true,\n message: response.message || 'Review submitted successfully',\n reviewId: response.reviewId,\n status: response.status,\n data: response.data\n });\n\n } catch (error) {\n console.error('Unexpected error in review submit handler:', error);\n \n // Try to call error callback\n if (config.onError) {\n try {\n await config.onError({} as ReviewFormData, error instanceof Error ? error : new Error(String(error)));\n } catch (callbackError) {\n console.error('Error callback failed:', callbackError);\n }\n }\n\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Create a generic NextJS API route handler for fetching reviews\n */\nexport function createReviewFetchHandler(config: ReviewHandlerConfig) {\n return async function reviewFetchHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow GET requests\n if (request.method !== 'GET') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET' } }\n );\n }\n\n try {\n // Parse query parameters\n const searchParams = request.nextUrl.searchParams;\n const params = {\n limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined,\n offset: searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : undefined,\n featured: searchParams.get('featured') === 'true' ? true : undefined,\n minRating: searchParams.get('minRating') ? parseInt(searchParams.get('minRating')!) : undefined,\n sortBy: searchParams.get('sortBy') as 'date' | 'rating' | 'name' | undefined,\n sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc' | undefined,\n };\n\n // Call the user-provided fetch function\n let reviews: Review[];\n try {\n if (!config.onFetch) {\n return NextResponse.json(\n { error: 'Review fetching not configured' },\n { status: 500 }\n );\n }\n\n reviews = await config.onFetch(params);\n } catch (fetchError) {\n console.error('Review fetch error:', fetchError);\n \n return NextResponse.json(\n { error: 'Failed to fetch reviews. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from fetch function\n if (!Array.isArray(reviews)) {\n console.error('Invalid response from fetch function, expected array:', reviews);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Return successful response\n return NextResponse.json(reviews);\n\n } catch (error) {\n console.error('Unexpected error in review fetch handler:', error);\n \n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Combined handler that supports both GET (fetch) and POST (submit)\n */\nexport function createReviewHandler(config: ReviewHandlerConfig) {\n const fetchHandler = createReviewFetchHandler(config);\n const submitHandler = createReviewSubmitHandler(config);\n\n return async function combinedHandler(request: NextRequest): Promise<NextResponse> {\n if (request.method === 'GET') {\n return fetchHandler(request);\n } else if (request.method === 'POST') {\n return submitHandler(request);\n } else {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET, POST' } }\n );\n }\n };\n}\n\n/**\n * Helper function to create CORS headers for the response\n */\nexport function createCorsHeaders(allowedOrigins?: string[]): HeadersInit {\n const headers: HeadersInit = {\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n };\n\n if (allowedOrigins && allowedOrigins.length > 0) {\n headers['Access-Control-Allow-Origin'] = allowedOrigins.join(', ');\n } else {\n headers['Access-Control-Allow-Origin'] = '*';\n }\n\n return headers;\n}\n\n/**\n * Helper to handle OPTIONS requests for CORS\n */\nexport function handleOptions(allowedOrigins?: string[]): NextResponse {\n return new NextResponse(null, {\n status: 200,\n headers: createCorsHeaders(allowedOrigins)\n });\n}","/**\n * Server-side reCAPTCHA verification utility\n */\n\nexport interface RecaptchaConfig {\n secretKey: string;\n minimumScore?: number; // For reCAPTCHA v3, minimum score (0.0 to 1.0)\n timeoutMs?: number; // Request timeout in milliseconds\n}\n\nexport interface RecaptchaVerificationResult {\n success: boolean;\n score?: number; // reCAPTCHA v3 score\n action?: string; // reCAPTCHA v3 action\n challengeTimestamp?: string;\n hostname?: string;\n errorCodes?: string[];\n}\n\n/**\n * Verify reCAPTCHA token with Google's API\n */\nexport async function verifyRecaptcha(\n token: string,\n config: RecaptchaConfig\n): Promise<RecaptchaVerificationResult> {\n if (!config.secretKey) {\n console.warn('RECAPTCHA_SECRET_KEY not configured, skipping verification');\n return { success: true };\n }\n\n if (!token || typeof token !== 'string') {\n return {\n success: false,\n errorCodes: ['missing-input-response']\n };\n }\n\n const timeout = config.timeoutMs || 10000; // 10 second default timeout\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n secret: config.secretKey,\n response: token\n }),\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.error('reCAPTCHA API returned non-OK status:', response.status);\n return {\n success: false,\n errorCodes: ['recaptcha-api-error']\n };\n }\n\n const data = await response.json();\n \n // Check minimum score for v3 (if configured)\n if (config.minimumScore !== undefined && data.score !== undefined) {\n if (data.score < config.minimumScore) {\n return {\n success: false,\n score: data.score,\n errorCodes: ['score-threshold-not-met']\n };\n }\n }\n\n return {\n success: data.success === true,\n score: data.score,\n action: data.action,\n challengeTimestamp: data.challenge_ts,\n hostname: data.hostname,\n errorCodes: data['error-codes'] || []\n };\n\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n console.error('reCAPTCHA verification timeout');\n return {\n success: false,\n errorCodes: ['timeout-or-duplicate']\n };\n }\n\n console.error('reCAPTCHA verification error:', error);\n return {\n success: false,\n errorCodes: ['network-error']\n };\n }\n}\n\n/**\n * Get human-readable error message for reCAPTCHA error codes\n */\nexport function getRecaptchaErrorMessage(errorCodes: string[]): string {\n const errorMessages: { [key: string]: string } = {\n 'missing-input-secret': 'reCAPTCHA secret key is missing',\n 'invalid-input-secret': 'reCAPTCHA secret key is invalid',\n 'missing-input-response': 'reCAPTCHA token is missing',\n 'invalid-input-response': 'reCAPTCHA token is invalid or malformed',\n 'bad-request': 'The request is invalid or malformed',\n 'timeout-or-duplicate': 'reCAPTCHA verification timed out or token was already used',\n 'score-threshold-not-met': 'reCAPTCHA score is below the required threshold',\n 'recaptcha-api-error': 'reCAPTCHA service is unavailable',\n 'network-error': 'Network error during reCAPTCHA verification'\n };\n\n if (!errorCodes || errorCodes.length === 0) {\n return 'reCAPTCHA verification failed';\n }\n\n const knownErrors = errorCodes\n .filter(code => errorMessages[code])\n .map(code => errorMessages[code]);\n\n return knownErrors.length > 0 \n ? knownErrors.join(', ')\n : 'reCAPTCHA verification failed';\n}","import { createReviewSubmitHandler, createReviewFetchHandler } from './handler';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\n\n/**\n * Configuration for BusinessFlow CRM review integration\n */\nexport interface BusinessFlowReviewConfig {\n /** BusinessFlow API base URL (e.g., https://api.businessflow.co.za) */\n apiUrl: string;\n /** BusinessFlow API key for authentication */\n apiKey: string;\n /** Source URL for review attribution (optional, will use NEXT_PUBLIC_SITE_URL if not provided) */\n sourceUrl?: string;\n /** reCAPTCHA secret key for server-side verification (optional) */\n recaptchaSecret?: string;\n /** Minimum reCAPTCHA score for v3 (optional, defaults to 0.5) */\n minimumScore?: number;\n /** Custom success callback after review is submitted to BusinessFlow */\n onSuccess?: (data: ReviewFormData, response: ReviewApiResponse) => Promise<void>;\n /** Custom error callback if submission fails */\n onError?: (data: ReviewFormData, error: Error) => Promise<void>;\n}\n\n/**\n * Create a NextJS API route handler that submits reviews to BusinessFlow CRM\n *\n * @example\n * ```typescript\n * // app/api/reviews/submit/route.ts\n * import { createBusinessFlowReviewHandler } from '@businessflow/reviews/server';\n *\n * export const POST = createBusinessFlowReviewHandler({\n * apiUrl: process.env.BUSINESS_FLOW_API_URL!,\n * apiKey: process.env.BUSINESS_FLOW_API_KEY!,\n * sourceUrl: process.env.SITE_URL!,\n * recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY!,\n * });\n * ```\n */\nexport function createBusinessFlowReviewHandler(config: BusinessFlowReviewConfig) {\n const handlerConfig: ReviewHandlerConfig = {\n onSubmit: async (data: ReviewFormData): Promise<ReviewApiResponse> => {\n // Remove token from data if present (already verified by reCAPTCHA handler)\n const { token, ...reviewData } = data;\n\n // Map fields to BusinessFlow API format\n const payload = {\n ReviewerName: reviewData.reviewerName,\n ReviewerEmail: reviewData.reviewerEmail,\n Rating: reviewData.rating,\n Content: reviewData.content || '',\n };\n\n try {\n const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Submit`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(payload),\n });\n\n const responseText = await response.text();\n let result;\n\n try {\n result = JSON.parse(responseText);\n } catch {\n // If response isn't JSON, create a response object\n result = {\n success: response.ok,\n message: response.ok ? 'Review submitted successfully' : responseText || 'Unknown error',\n };\n }\n\n if (!response.ok) {\n return {\n success: false,\n message: result.message || `HTTP ${response.status}: ${response.statusText}`,\n data: result,\n };\n }\n\n return {\n success: true,\n message: result.message || 'Review submitted successfully',\n reviewId: result.reviewId,\n status: result.status,\n data: result,\n };\n } catch (error) {\n console.error('BusinessFlow Review API error:', error);\n return {\n success: false,\n message: error instanceof Error ? error.message : 'Network error occurred',\n };\n }\n },\n\n // Configure reCAPTCHA if provided\n recaptcha: config.recaptchaSecret ? {\n secretKey: config.recaptchaSecret,\n minimumScore: config.minimumScore ?? 0.5,\n } : undefined,\n\n // Pass through callbacks\n onSuccess: config.onSuccess,\n onError: config.onError,\n };\n\n return createReviewSubmitHandler(handlerConfig);\n}\n\n/**\n * Create a NextJS API route handler that fetches featured reviews from BusinessFlow CRM\n *\n * @example\n * ```typescript\n * // app/api/reviews/featured/route.ts\n * import { createBusinessFlowFeaturedHandler } from '@businessflow/reviews/server';\n *\n * export const GET = createBusinessFlowFeaturedHandler({\n * apiUrl: process.env.BUSINESS_FLOW_API_URL!,\n * apiKey: process.env.BUSINESS_FLOW_API_KEY!,\n * });\n * ```\n */\nexport function createBusinessFlowFeaturedHandler(config: Pick<BusinessFlowReviewConfig, 'apiUrl' | 'apiKey'>) {\n return createReviewFetchHandler({\n onFetch: async (params?: any): Promise<Review[]> => {\n const limit = params?.limit || 10;\n\n try {\n const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Featured?limit=${limit}`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to fetch featured reviews: ${errorText}`);\n }\n\n const data = await response.json();\n\n // Transform BusinessFlow response to Review interface\n return data.map((item: any) => ({\n id: `review_${Date.now()}_${Math.random()}`, // Generate unique ID\n reviewerName: item.reviewerName,\n reviewerEmail: '', // Not included in public response\n rating: item.rating,\n content: item.content,\n createdAt: item.submittedAt,\n featured: true,\n approved: true,\n }));\n } catch (error) {\n console.error('BusinessFlow Featured Reviews API error:', error);\n throw error;\n }\n },\n });\n}\n\n/**\n * Simplified BusinessFlow review handler for basic use cases\n * Uses environment variables for configuration\n *\n * @example\n * ```typescript\n * // app/api/reviews/submit/route.ts\n * import { createSimpleBusinessFlowReviewHandler } from '@businessflow/reviews/server';\n *\n * export const POST = createSimpleBusinessFlowReviewHandler();\n * ```\n *\n * Required environment variables:\n * - BUSINESS_FLOW_API_URL\n * - BUSINESS_FLOW_API_KEY\n * - RECAPTCHA_SECRET_KEY (optional)\n * - NEXT_PUBLIC_SITE_URL (optional, for sourceUrl)\n */\nexport function createSimpleBusinessFlowReviewHandler() {\n const apiUrl = process.env.BUSINESS_FLOW_API_URL;\n const apiKey = process.env.BUSINESS_FLOW_API_KEY;\n\n if (!apiUrl || !apiKey) {\n throw new Error(\n 'Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set'\n );\n }\n\n return createBusinessFlowReviewHandler({\n apiUrl,\n apiKey,\n sourceUrl: process.env.NEXT_PUBLIC_SITE_URL,\n recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY,\n });\n}\n\n/**\n * Simplified BusinessFlow featured reviews handler for basic use cases\n * Uses environment variables for configuration\n *\n * @example\n * ```typescript\n * // app/api/reviews/featured/route.ts\n * import { createSimpleBusinessFlowFeaturedHandler } from '@businessflow/reviews/server';\n *\n * export const GET = createSimpleBusinessFlowFeaturedHandler();\n * ```\n */\nexport function createSimpleBusinessFlowFeaturedHandler() {\n const apiUrl = process.env.BUSINESS_FLOW_API_URL;\n const apiKey = process.env.BUSINESS_FLOW_API_KEY;\n\n if (!apiUrl || !apiKey) {\n throw new Error(\n 'Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set'\n );\n }\n\n return createBusinessFlowFeaturedHandler({\n apiUrl,\n apiKey,\n });\n}"],"mappings":"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","names":[]}
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,94 +1,5 @@
|
|
|
1
1
|
// src/server/handler.ts
|
|
2
2
|
import { NextResponse } from "next/server";
|
|
3
|
-
|
|
4
|
-
// src/server/recaptcha.ts
|
|
5
|
-
async function verifyRecaptcha(token, config) {
|
|
6
|
-
if (!config.secretKey) {
|
|
7
|
-
console.warn("RECAPTCHA_SECRET_KEY not configured, skipping verification");
|
|
8
|
-
return { success: true };
|
|
9
|
-
}
|
|
10
|
-
if (!token || typeof token !== "string") {
|
|
11
|
-
return {
|
|
12
|
-
success: false,
|
|
13
|
-
errorCodes: ["missing-input-response"]
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
const timeout = config.timeoutMs || 1e4;
|
|
17
|
-
try {
|
|
18
|
-
const controller = new AbortController();
|
|
19
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
20
|
-
const response = await fetch("https://www.google.com/recaptcha/api/siteverify", {
|
|
21
|
-
method: "POST",
|
|
22
|
-
headers: {
|
|
23
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
24
|
-
},
|
|
25
|
-
body: new URLSearchParams({
|
|
26
|
-
secret: config.secretKey,
|
|
27
|
-
response: token
|
|
28
|
-
}),
|
|
29
|
-
signal: controller.signal
|
|
30
|
-
});
|
|
31
|
-
clearTimeout(timeoutId);
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
console.error("reCAPTCHA API returned non-OK status:", response.status);
|
|
34
|
-
return {
|
|
35
|
-
success: false,
|
|
36
|
-
errorCodes: ["recaptcha-api-error"]
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const data = await response.json();
|
|
40
|
-
if (config.minimumScore !== void 0 && data.score !== void 0) {
|
|
41
|
-
if (data.score < config.minimumScore) {
|
|
42
|
-
return {
|
|
43
|
-
success: false,
|
|
44
|
-
score: data.score,
|
|
45
|
-
errorCodes: ["score-threshold-not-met"]
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
success: data.success === true,
|
|
51
|
-
score: data.score,
|
|
52
|
-
action: data.action,
|
|
53
|
-
challengeTimestamp: data.challenge_ts,
|
|
54
|
-
hostname: data.hostname,
|
|
55
|
-
errorCodes: data["error-codes"] || []
|
|
56
|
-
};
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
59
|
-
console.error("reCAPTCHA verification timeout");
|
|
60
|
-
return {
|
|
61
|
-
success: false,
|
|
62
|
-
errorCodes: ["timeout-or-duplicate"]
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
console.error("reCAPTCHA verification error:", error);
|
|
66
|
-
return {
|
|
67
|
-
success: false,
|
|
68
|
-
errorCodes: ["network-error"]
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function getRecaptchaErrorMessage(errorCodes) {
|
|
73
|
-
const errorMessages = {
|
|
74
|
-
"missing-input-secret": "reCAPTCHA secret key is missing",
|
|
75
|
-
"invalid-input-secret": "reCAPTCHA secret key is invalid",
|
|
76
|
-
"missing-input-response": "reCAPTCHA token is missing",
|
|
77
|
-
"invalid-input-response": "reCAPTCHA token is invalid or malformed",
|
|
78
|
-
"bad-request": "The request is invalid or malformed",
|
|
79
|
-
"timeout-or-duplicate": "reCAPTCHA verification timed out or token was already used",
|
|
80
|
-
"score-threshold-not-met": "reCAPTCHA score is below the required threshold",
|
|
81
|
-
"recaptcha-api-error": "reCAPTCHA service is unavailable",
|
|
82
|
-
"network-error": "Network error during reCAPTCHA verification"
|
|
83
|
-
};
|
|
84
|
-
if (!errorCodes || errorCodes.length === 0) {
|
|
85
|
-
return "reCAPTCHA verification failed";
|
|
86
|
-
}
|
|
87
|
-
const knownErrors = errorCodes.filter((code) => errorMessages[code]).map((code) => errorMessages[code]);
|
|
88
|
-
return knownErrors.length > 0 ? knownErrors.join(", ") : "reCAPTCHA verification failed";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// src/server/handler.ts
|
|
92
3
|
function createReviewSubmitHandler(config) {
|
|
93
4
|
return async function reviewSubmitHandler(request) {
|
|
94
5
|
if (request.method !== "POST") {
|
|
@@ -196,16 +107,6 @@ function createReviewSubmitHandler(config) {
|
|
|
196
107
|
}
|
|
197
108
|
}
|
|
198
109
|
}
|
|
199
|
-
if (config.recaptcha && body.RecaptchaToken) {
|
|
200
|
-
const recaptchaResult = await verifyRecaptcha(body.RecaptchaToken, config.recaptcha);
|
|
201
|
-
if (!recaptchaResult.success) {
|
|
202
|
-
const errorMessage = getRecaptchaErrorMessage(recaptchaResult.errorCodes || []);
|
|
203
|
-
return NextResponse.json(
|
|
204
|
-
{ error: `reCAPTCHA verification failed: ${errorMessage}` },
|
|
205
|
-
{ status: 400 }
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
110
|
let response;
|
|
210
111
|
try {
|
|
211
112
|
if (!config.onSubmit) {
|
|
@@ -357,11 +258,230 @@ function handleOptions(allowedOrigins) {
|
|
|
357
258
|
headers: createCorsHeaders(allowedOrigins)
|
|
358
259
|
});
|
|
359
260
|
}
|
|
261
|
+
|
|
262
|
+
// src/server/recaptcha.ts
|
|
263
|
+
async function verifyRecaptcha(token, config) {
|
|
264
|
+
if (!config.secretKey) {
|
|
265
|
+
console.warn("RECAPTCHA_SECRET_KEY not configured, skipping verification");
|
|
266
|
+
return { success: true };
|
|
267
|
+
}
|
|
268
|
+
if (!token || typeof token !== "string") {
|
|
269
|
+
return {
|
|
270
|
+
success: false,
|
|
271
|
+
errorCodes: ["missing-input-response"]
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const timeout = config.timeoutMs || 1e4;
|
|
275
|
+
try {
|
|
276
|
+
const controller = new AbortController();
|
|
277
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
278
|
+
const response = await fetch("https://www.google.com/recaptcha/api/siteverify", {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: {
|
|
281
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
282
|
+
},
|
|
283
|
+
body: new URLSearchParams({
|
|
284
|
+
secret: config.secretKey,
|
|
285
|
+
response: token
|
|
286
|
+
}),
|
|
287
|
+
signal: controller.signal
|
|
288
|
+
});
|
|
289
|
+
clearTimeout(timeoutId);
|
|
290
|
+
if (!response.ok) {
|
|
291
|
+
console.error("reCAPTCHA API returned non-OK status:", response.status);
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
errorCodes: ["recaptcha-api-error"]
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
if (config.minimumScore !== void 0 && data.score !== void 0) {
|
|
299
|
+
if (data.score < config.minimumScore) {
|
|
300
|
+
return {
|
|
301
|
+
success: false,
|
|
302
|
+
score: data.score,
|
|
303
|
+
errorCodes: ["score-threshold-not-met"]
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
success: data.success === true,
|
|
309
|
+
score: data.score,
|
|
310
|
+
action: data.action,
|
|
311
|
+
challengeTimestamp: data.challenge_ts,
|
|
312
|
+
hostname: data.hostname,
|
|
313
|
+
errorCodes: data["error-codes"] || []
|
|
314
|
+
};
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
317
|
+
console.error("reCAPTCHA verification timeout");
|
|
318
|
+
return {
|
|
319
|
+
success: false,
|
|
320
|
+
errorCodes: ["timeout-or-duplicate"]
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
console.error("reCAPTCHA verification error:", error);
|
|
324
|
+
return {
|
|
325
|
+
success: false,
|
|
326
|
+
errorCodes: ["network-error"]
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function getRecaptchaErrorMessage(errorCodes) {
|
|
331
|
+
const errorMessages = {
|
|
332
|
+
"missing-input-secret": "reCAPTCHA secret key is missing",
|
|
333
|
+
"invalid-input-secret": "reCAPTCHA secret key is invalid",
|
|
334
|
+
"missing-input-response": "reCAPTCHA token is missing",
|
|
335
|
+
"invalid-input-response": "reCAPTCHA token is invalid or malformed",
|
|
336
|
+
"bad-request": "The request is invalid or malformed",
|
|
337
|
+
"timeout-or-duplicate": "reCAPTCHA verification timed out or token was already used",
|
|
338
|
+
"score-threshold-not-met": "reCAPTCHA score is below the required threshold",
|
|
339
|
+
"recaptcha-api-error": "reCAPTCHA service is unavailable",
|
|
340
|
+
"network-error": "Network error during reCAPTCHA verification"
|
|
341
|
+
};
|
|
342
|
+
if (!errorCodes || errorCodes.length === 0) {
|
|
343
|
+
return "reCAPTCHA verification failed";
|
|
344
|
+
}
|
|
345
|
+
const knownErrors = errorCodes.filter((code) => errorMessages[code]).map((code) => errorMessages[code]);
|
|
346
|
+
return knownErrors.length > 0 ? knownErrors.join(", ") : "reCAPTCHA verification failed";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/server/businessflow.ts
|
|
350
|
+
function createBusinessFlowReviewHandler(config) {
|
|
351
|
+
const handlerConfig = {
|
|
352
|
+
onSubmit: async (data) => {
|
|
353
|
+
const { token, ...reviewData } = data;
|
|
354
|
+
const payload = {
|
|
355
|
+
ReviewerName: reviewData.reviewerName,
|
|
356
|
+
ReviewerEmail: reviewData.reviewerEmail,
|
|
357
|
+
Rating: reviewData.rating,
|
|
358
|
+
Content: reviewData.content || ""
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Submit`, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: {
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
"X-API-Key": config.apiKey
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify(payload)
|
|
368
|
+
});
|
|
369
|
+
const responseText = await response.text();
|
|
370
|
+
let result;
|
|
371
|
+
try {
|
|
372
|
+
result = JSON.parse(responseText);
|
|
373
|
+
} catch {
|
|
374
|
+
result = {
|
|
375
|
+
success: response.ok,
|
|
376
|
+
message: response.ok ? "Review submitted successfully" : responseText || "Unknown error"
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
if (!response.ok) {
|
|
380
|
+
return {
|
|
381
|
+
success: false,
|
|
382
|
+
message: result.message || `HTTP ${response.status}: ${response.statusText}`,
|
|
383
|
+
data: result
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
message: result.message || "Review submitted successfully",
|
|
389
|
+
reviewId: result.reviewId,
|
|
390
|
+
status: result.status,
|
|
391
|
+
data: result
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
console.error("BusinessFlow Review API error:", error);
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
message: error instanceof Error ? error.message : "Network error occurred"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
// Configure reCAPTCHA if provided
|
|
402
|
+
recaptcha: config.recaptchaSecret ? {
|
|
403
|
+
secretKey: config.recaptchaSecret,
|
|
404
|
+
minimumScore: config.minimumScore ?? 0.5
|
|
405
|
+
} : void 0,
|
|
406
|
+
// Pass through callbacks
|
|
407
|
+
onSuccess: config.onSuccess,
|
|
408
|
+
onError: config.onError
|
|
409
|
+
};
|
|
410
|
+
return createReviewSubmitHandler(handlerConfig);
|
|
411
|
+
}
|
|
412
|
+
function createBusinessFlowFeaturedHandler(config) {
|
|
413
|
+
return createReviewFetchHandler({
|
|
414
|
+
onFetch: async (params) => {
|
|
415
|
+
const limit = params?.limit || 10;
|
|
416
|
+
try {
|
|
417
|
+
const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Featured?limit=${limit}`, {
|
|
418
|
+
method: "GET",
|
|
419
|
+
headers: {
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
"X-API-Key": config.apiKey
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
const errorText = await response.text();
|
|
426
|
+
throw new Error(`Failed to fetch featured reviews: ${errorText}`);
|
|
427
|
+
}
|
|
428
|
+
const data = await response.json();
|
|
429
|
+
return data.map((item) => ({
|
|
430
|
+
id: `review_${Date.now()}_${Math.random()}`,
|
|
431
|
+
// Generate unique ID
|
|
432
|
+
reviewerName: item.reviewerName,
|
|
433
|
+
reviewerEmail: "",
|
|
434
|
+
// Not included in public response
|
|
435
|
+
rating: item.rating,
|
|
436
|
+
content: item.content,
|
|
437
|
+
createdAt: item.submittedAt,
|
|
438
|
+
featured: true,
|
|
439
|
+
approved: true
|
|
440
|
+
}));
|
|
441
|
+
} catch (error) {
|
|
442
|
+
console.error("BusinessFlow Featured Reviews API error:", error);
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
function createSimpleBusinessFlowReviewHandler() {
|
|
449
|
+
const apiUrl = process.env.BUSINESS_FLOW_API_URL;
|
|
450
|
+
const apiKey = process.env.BUSINESS_FLOW_API_KEY;
|
|
451
|
+
if (!apiUrl || !apiKey) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set"
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
return createBusinessFlowReviewHandler({
|
|
457
|
+
apiUrl,
|
|
458
|
+
apiKey,
|
|
459
|
+
sourceUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
|
460
|
+
recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function createSimpleBusinessFlowFeaturedHandler() {
|
|
464
|
+
const apiUrl = process.env.BUSINESS_FLOW_API_URL;
|
|
465
|
+
const apiKey = process.env.BUSINESS_FLOW_API_KEY;
|
|
466
|
+
if (!apiUrl || !apiKey) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
"Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return createBusinessFlowFeaturedHandler({
|
|
472
|
+
apiUrl,
|
|
473
|
+
apiKey
|
|
474
|
+
});
|
|
475
|
+
}
|
|
360
476
|
export {
|
|
477
|
+
createBusinessFlowFeaturedHandler,
|
|
478
|
+
createBusinessFlowReviewHandler,
|
|
361
479
|
createCorsHeaders,
|
|
362
480
|
createReviewFetchHandler,
|
|
363
481
|
createReviewHandler,
|
|
364
482
|
createReviewSubmitHandler,
|
|
483
|
+
createSimpleBusinessFlowFeaturedHandler,
|
|
484
|
+
createSimpleBusinessFlowReviewHandler,
|
|
365
485
|
getRecaptchaErrorMessage,
|
|
366
486
|
handleOptions,
|
|
367
487
|
verifyRecaptcha
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/handler.ts","../../src/server/recaptcha.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\nimport { verifyRecaptcha, getRecaptchaErrorMessage } from './recaptcha';\n\n/**\n * Create a generic NextJS API route handler for review submission\n */\nexport function createReviewSubmitHandler(config: ReviewHandlerConfig) {\n return async function reviewSubmitHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow POST requests\n if (request.method !== 'POST') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'POST' } }\n );\n }\n\n try {\n // Parse request body\n let body: ReviewFormData;\n try {\n body = await request.json();\n } catch (parseError) {\n return NextResponse.json(\n { error: 'Invalid JSON in request body' },\n { status: 400 }\n );\n }\n\n // Rate limiting (if configured)\n if (config.rateLimiter) {\n const allowed = await config.rateLimiter(request);\n if (!allowed) {\n return NextResponse.json(\n { error: 'Too many requests. Please try again later.' },\n { status: 429 }\n );\n }\n }\n\n // Validate required fields (basic validation)\n if (!body.reviewerName || typeof body.reviewerName !== 'string' || body.reviewerName.trim().length === 0) {\n return NextResponse.json(\n { error: 'Reviewer name is required' },\n { status: 400 }\n );\n }\n\n if (!body.reviewerEmail || typeof body.reviewerEmail !== 'string' || body.reviewerEmail.trim().length === 0) {\n return NextResponse.json(\n { error: 'Email is required' },\n { status: 400 }\n );\n }\n\n // Email format validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(body.reviewerEmail.trim())) {\n return NextResponse.json(\n { error: 'Please enter a valid email address' },\n { status: 400 }\n );\n }\n\n // Rating validation\n if (typeof body.rating !== 'number' || body.rating < 1 || body.rating > 5 || !Number.isInteger(body.rating)) {\n return NextResponse.json(\n { error: 'Rating must be between 1 and 5 stars' },\n { status: 400 }\n );\n }\n\n // Custom validation (if configured)\n if (config.validation) {\n for (const [field, rules] of Object.entries(config.validation)) {\n const value = (body as any)[field];\n \n for (const rule of rules) {\n let isValid = true;\n let errorMessage = rule.message || 'Validation failed';\n\n switch (rule.type) {\n case 'required':\n isValid = value != null && value !== '' && \n (typeof value !== 'string' || value.trim() !== '');\n break;\n \n case 'email':\n if (value) {\n isValid = emailRegex.test(String(value).trim());\n }\n break;\n \n case 'rating':\n if (value !== undefined) {\n isValid = typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 5;\n }\n break;\n \n case 'minLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length >= rule.value;\n }\n break;\n \n case 'maxLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length <= rule.value;\n }\n break;\n \n case 'pattern':\n if (value && typeof rule.value === 'string') {\n const regex = new RegExp(rule.value);\n isValid = regex.test(String(value));\n }\n break;\n \n case 'custom':\n if (rule.validator) {\n const result = rule.validator(value);\n isValid = result === true;\n if (typeof result === 'string') {\n errorMessage = result;\n }\n }\n break;\n }\n\n if (!isValid) {\n return NextResponse.json(\n { error: errorMessage },\n { status: 400 }\n );\n }\n }\n }\n }\n\n // reCAPTCHA verification (if configured)\n if (config.recaptcha && (body as any).RecaptchaToken) {\n const recaptchaResult = await verifyRecaptcha((body as any).RecaptchaToken, config.recaptcha);\n \n if (!recaptchaResult.success) {\n const errorMessage = getRecaptchaErrorMessage(recaptchaResult.errorCodes || []);\n return NextResponse.json(\n { error: `reCAPTCHA verification failed: ${errorMessage}` },\n { status: 400 }\n );\n }\n }\n\n // Call the user-provided submit function\n let response: ReviewApiResponse;\n try {\n if (!config.onSubmit) {\n return NextResponse.json(\n { error: 'Review submission not configured' },\n { status: 500 }\n );\n }\n\n response = await config.onSubmit(body);\n } catch (submitError) {\n console.error('Review submission error:', submitError);\n \n // Call error callback if provided\n if (config.onError) {\n await config.onError(body, submitError instanceof Error ? submitError : new Error(String(submitError)));\n }\n\n return NextResponse.json(\n { error: 'Failed to submit review. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from submit function\n if (!response || typeof response !== 'object') {\n console.error('Invalid response from submit function:', response);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Handle unsuccessful submission\n if (!response.success) {\n const statusCode = response.message?.includes('validation') ? 400 : 500;\n return NextResponse.json(\n { \n error: response.message || 'Failed to submit review',\n data: response.data \n },\n { status: statusCode }\n );\n }\n\n // Call success callback if provided\n if (config.onSuccess) {\n try {\n await config.onSuccess(body, response);\n } catch (callbackError) {\n console.error('Success callback error:', callbackError);\n // Don't fail the request if callback fails\n }\n }\n\n // Return successful response\n return NextResponse.json({\n success: true,\n message: response.message || 'Review submitted successfully',\n reviewId: response.reviewId,\n status: response.status,\n data: response.data\n });\n\n } catch (error) {\n console.error('Unexpected error in review submit handler:', error);\n \n // Try to call error callback\n if (config.onError) {\n try {\n await config.onError({} as ReviewFormData, error instanceof Error ? error : new Error(String(error)));\n } catch (callbackError) {\n console.error('Error callback failed:', callbackError);\n }\n }\n\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Create a generic NextJS API route handler for fetching reviews\n */\nexport function createReviewFetchHandler(config: ReviewHandlerConfig) {\n return async function reviewFetchHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow GET requests\n if (request.method !== 'GET') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET' } }\n );\n }\n\n try {\n // Parse query parameters\n const searchParams = request.nextUrl.searchParams;\n const params = {\n limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined,\n offset: searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : undefined,\n featured: searchParams.get('featured') === 'true' ? true : undefined,\n minRating: searchParams.get('minRating') ? parseInt(searchParams.get('minRating')!) : undefined,\n sortBy: searchParams.get('sortBy') as 'date' | 'rating' | 'name' | undefined,\n sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc' | undefined,\n };\n\n // Call the user-provided fetch function\n let reviews: Review[];\n try {\n if (!config.onFetch) {\n return NextResponse.json(\n { error: 'Review fetching not configured' },\n { status: 500 }\n );\n }\n\n reviews = await config.onFetch(params);\n } catch (fetchError) {\n console.error('Review fetch error:', fetchError);\n \n return NextResponse.json(\n { error: 'Failed to fetch reviews. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from fetch function\n if (!Array.isArray(reviews)) {\n console.error('Invalid response from fetch function, expected array:', reviews);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Return successful response\n return NextResponse.json(reviews);\n\n } catch (error) {\n console.error('Unexpected error in review fetch handler:', error);\n \n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Combined handler that supports both GET (fetch) and POST (submit)\n */\nexport function createReviewHandler(config: ReviewHandlerConfig) {\n const fetchHandler = createReviewFetchHandler(config);\n const submitHandler = createReviewSubmitHandler(config);\n\n return async function combinedHandler(request: NextRequest): Promise<NextResponse> {\n if (request.method === 'GET') {\n return fetchHandler(request);\n } else if (request.method === 'POST') {\n return submitHandler(request);\n } else {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET, POST' } }\n );\n }\n };\n}\n\n/**\n * Helper function to create CORS headers for the response\n */\nexport function createCorsHeaders(allowedOrigins?: string[]): HeadersInit {\n const headers: HeadersInit = {\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n };\n\n if (allowedOrigins && allowedOrigins.length > 0) {\n headers['Access-Control-Allow-Origin'] = allowedOrigins.join(', ');\n } else {\n headers['Access-Control-Allow-Origin'] = '*';\n }\n\n return headers;\n}\n\n/**\n * Helper to handle OPTIONS requests for CORS\n */\nexport function handleOptions(allowedOrigins?: string[]): NextResponse {\n return new NextResponse(null, {\n status: 200,\n headers: createCorsHeaders(allowedOrigins)\n });\n}","/**\n * Server-side reCAPTCHA verification utility\n */\n\nexport interface RecaptchaConfig {\n secretKey: string;\n minimumScore?: number; // For reCAPTCHA v3, minimum score (0.0 to 1.0)\n timeoutMs?: number; // Request timeout in milliseconds\n}\n\nexport interface RecaptchaVerificationResult {\n success: boolean;\n score?: number; // reCAPTCHA v3 score\n action?: string; // reCAPTCHA v3 action\n challengeTimestamp?: string;\n hostname?: string;\n errorCodes?: string[];\n}\n\n/**\n * Verify reCAPTCHA token with Google's API\n */\nexport async function verifyRecaptcha(\n token: string,\n config: RecaptchaConfig\n): Promise<RecaptchaVerificationResult> {\n if (!config.secretKey) {\n console.warn('RECAPTCHA_SECRET_KEY not configured, skipping verification');\n return { success: true };\n }\n\n if (!token || typeof token !== 'string') {\n return {\n success: false,\n errorCodes: ['missing-input-response']\n };\n }\n\n const timeout = config.timeoutMs || 10000; // 10 second default timeout\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n secret: config.secretKey,\n response: token\n }),\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.error('reCAPTCHA API returned non-OK status:', response.status);\n return {\n success: false,\n errorCodes: ['recaptcha-api-error']\n };\n }\n\n const data = await response.json();\n \n // Check minimum score for v3 (if configured)\n if (config.minimumScore !== undefined && data.score !== undefined) {\n if (data.score < config.minimumScore) {\n return {\n success: false,\n score: data.score,\n errorCodes: ['score-threshold-not-met']\n };\n }\n }\n\n return {\n success: data.success === true,\n score: data.score,\n action: data.action,\n challengeTimestamp: data.challenge_ts,\n hostname: data.hostname,\n errorCodes: data['error-codes'] || []\n };\n\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n console.error('reCAPTCHA verification timeout');\n return {\n success: false,\n errorCodes: ['timeout-or-duplicate']\n };\n }\n\n console.error('reCAPTCHA verification error:', error);\n return {\n success: false,\n errorCodes: ['network-error']\n };\n }\n}\n\n/**\n * Get human-readable error message for reCAPTCHA error codes\n */\nexport function getRecaptchaErrorMessage(errorCodes: string[]): string {\n const errorMessages: { [key: string]: string } = {\n 'missing-input-secret': 'reCAPTCHA secret key is missing',\n 'invalid-input-secret': 'reCAPTCHA secret key is invalid',\n 'missing-input-response': 'reCAPTCHA token is missing',\n 'invalid-input-response': 'reCAPTCHA token is invalid or malformed',\n 'bad-request': 'The request is invalid or malformed',\n 'timeout-or-duplicate': 'reCAPTCHA verification timed out or token was already used',\n 'score-threshold-not-met': 'reCAPTCHA score is below the required threshold',\n 'recaptcha-api-error': 'reCAPTCHA service is unavailable',\n 'network-error': 'Network error during reCAPTCHA verification'\n };\n\n if (!errorCodes || errorCodes.length === 0) {\n return 'reCAPTCHA verification failed';\n }\n\n const knownErrors = errorCodes\n .filter(code => errorMessages[code])\n .map(code => errorMessages[code]);\n\n return knownErrors.length > 0 \n ? knownErrors.join(', ')\n : 'reCAPTCHA verification failed';\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/handler.ts","../../src/server/recaptcha.ts","../../src/server/businessflow.ts"],"sourcesContent":["import { NextRequest, NextResponse } from 'next/server';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\n\n/**\n * Create a generic NextJS API route handler for review submission\n */\nexport function createReviewSubmitHandler(config: ReviewHandlerConfig) {\n return async function reviewSubmitHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow POST requests\n if (request.method !== 'POST') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'POST' } }\n );\n }\n\n try {\n // Parse request body\n let body: ReviewFormData;\n try {\n body = await request.json();\n } catch (parseError) {\n return NextResponse.json(\n { error: 'Invalid JSON in request body' },\n { status: 400 }\n );\n }\n\n // Rate limiting (if configured)\n if (config.rateLimiter) {\n const allowed = await config.rateLimiter(request);\n if (!allowed) {\n return NextResponse.json(\n { error: 'Too many requests. Please try again later.' },\n { status: 429 }\n );\n }\n }\n\n // Validate required fields (basic validation)\n if (!body.reviewerName || typeof body.reviewerName !== 'string' || body.reviewerName.trim().length === 0) {\n return NextResponse.json(\n { error: 'Reviewer name is required' },\n { status: 400 }\n );\n }\n\n if (!body.reviewerEmail || typeof body.reviewerEmail !== 'string' || body.reviewerEmail.trim().length === 0) {\n return NextResponse.json(\n { error: 'Email is required' },\n { status: 400 }\n );\n }\n\n // Email format validation\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n if (!emailRegex.test(body.reviewerEmail.trim())) {\n return NextResponse.json(\n { error: 'Please enter a valid email address' },\n { status: 400 }\n );\n }\n\n // Rating validation\n if (typeof body.rating !== 'number' || body.rating < 1 || body.rating > 5 || !Number.isInteger(body.rating)) {\n return NextResponse.json(\n { error: 'Rating must be between 1 and 5 stars' },\n { status: 400 }\n );\n }\n\n // Custom validation (if configured)\n if (config.validation) {\n for (const [field, rules] of Object.entries(config.validation)) {\n const value = (body as any)[field];\n \n for (const rule of rules) {\n let isValid = true;\n let errorMessage = rule.message || 'Validation failed';\n\n switch (rule.type) {\n case 'required':\n isValid = value != null && value !== '' && \n (typeof value !== 'string' || value.trim() !== '');\n break;\n \n case 'email':\n if (value) {\n isValid = emailRegex.test(String(value).trim());\n }\n break;\n \n case 'rating':\n if (value !== undefined) {\n isValid = typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 5;\n }\n break;\n \n case 'minLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length >= rule.value;\n }\n break;\n \n case 'maxLength':\n if (value && typeof rule.value === 'number') {\n isValid = String(value).length <= rule.value;\n }\n break;\n \n case 'pattern':\n if (value && typeof rule.value === 'string') {\n const regex = new RegExp(rule.value);\n isValid = regex.test(String(value));\n }\n break;\n \n case 'custom':\n if (rule.validator) {\n const result = rule.validator(value);\n isValid = result === true;\n if (typeof result === 'string') {\n errorMessage = result;\n }\n }\n break;\n }\n\n if (!isValid) {\n return NextResponse.json(\n { error: errorMessage },\n { status: 400 }\n );\n }\n }\n }\n }\n\n // reCAPTCHA verification (if configured)\n // if (config.recaptcha && (body as any).RecaptchaToken) {\n // const recaptchaResult = await verifyRecaptcha((body as any).RecaptchaToken, config.recaptcha);\n \n // if (!recaptchaResult.success) {\n // const errorMessage = getRecaptchaErrorMessage(recaptchaResult.errorCodes || []);\n // return NextResponse.json(\n // { error: `reCAPTCHA verification failed: ${errorMessage}` },\n // { status: 400 }\n // );\n // }\n // }\n\n // Call the user-provided submit function\n let response: ReviewApiResponse;\n try {\n if (!config.onSubmit) {\n return NextResponse.json(\n { error: 'Review submission not configured' },\n { status: 500 }\n );\n }\n\n response = await config.onSubmit(body);\n } catch (submitError) {\n console.error('Review submission error:', submitError);\n \n // Call error callback if provided\n if (config.onError) {\n await config.onError(body, submitError instanceof Error ? submitError : new Error(String(submitError)));\n }\n\n return NextResponse.json(\n { error: 'Failed to submit review. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from submit function\n if (!response || typeof response !== 'object') {\n console.error('Invalid response from submit function:', response);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Handle unsuccessful submission\n if (!response.success) {\n const statusCode = response.message?.includes('validation') ? 400 : 500;\n return NextResponse.json(\n { \n error: response.message || 'Failed to submit review',\n data: response.data \n },\n { status: statusCode }\n );\n }\n\n // Call success callback if provided\n if (config.onSuccess) {\n try {\n await config.onSuccess(body, response);\n } catch (callbackError) {\n console.error('Success callback error:', callbackError);\n // Don't fail the request if callback fails\n }\n }\n\n // Return successful response\n return NextResponse.json({\n success: true,\n message: response.message || 'Review submitted successfully',\n reviewId: response.reviewId,\n status: response.status,\n data: response.data\n });\n\n } catch (error) {\n console.error('Unexpected error in review submit handler:', error);\n \n // Try to call error callback\n if (config.onError) {\n try {\n await config.onError({} as ReviewFormData, error instanceof Error ? error : new Error(String(error)));\n } catch (callbackError) {\n console.error('Error callback failed:', callbackError);\n }\n }\n\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Create a generic NextJS API route handler for fetching reviews\n */\nexport function createReviewFetchHandler(config: ReviewHandlerConfig) {\n return async function reviewFetchHandler(request: NextRequest): Promise<NextResponse> {\n // Only allow GET requests\n if (request.method !== 'GET') {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET' } }\n );\n }\n\n try {\n // Parse query parameters\n const searchParams = request.nextUrl.searchParams;\n const params = {\n limit: searchParams.get('limit') ? parseInt(searchParams.get('limit')!) : undefined,\n offset: searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : undefined,\n featured: searchParams.get('featured') === 'true' ? true : undefined,\n minRating: searchParams.get('minRating') ? parseInt(searchParams.get('minRating')!) : undefined,\n sortBy: searchParams.get('sortBy') as 'date' | 'rating' | 'name' | undefined,\n sortOrder: searchParams.get('sortOrder') as 'asc' | 'desc' | undefined,\n };\n\n // Call the user-provided fetch function\n let reviews: Review[];\n try {\n if (!config.onFetch) {\n return NextResponse.json(\n { error: 'Review fetching not configured' },\n { status: 500 }\n );\n }\n\n reviews = await config.onFetch(params);\n } catch (fetchError) {\n console.error('Review fetch error:', fetchError);\n \n return NextResponse.json(\n { error: 'Failed to fetch reviews. Please try again.' },\n { status: 500 }\n );\n }\n\n // Validate response from fetch function\n if (!Array.isArray(reviews)) {\n console.error('Invalid response from fetch function, expected array:', reviews);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n\n // Return successful response\n return NextResponse.json(reviews);\n\n } catch (error) {\n console.error('Unexpected error in review fetch handler:', error);\n \n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n };\n}\n\n/**\n * Combined handler that supports both GET (fetch) and POST (submit)\n */\nexport function createReviewHandler(config: ReviewHandlerConfig) {\n const fetchHandler = createReviewFetchHandler(config);\n const submitHandler = createReviewSubmitHandler(config);\n\n return async function combinedHandler(request: NextRequest): Promise<NextResponse> {\n if (request.method === 'GET') {\n return fetchHandler(request);\n } else if (request.method === 'POST') {\n return submitHandler(request);\n } else {\n return NextResponse.json(\n { error: 'Method not allowed' },\n { status: 405, headers: { Allow: 'GET, POST' } }\n );\n }\n };\n}\n\n/**\n * Helper function to create CORS headers for the response\n */\nexport function createCorsHeaders(allowedOrigins?: string[]): HeadersInit {\n const headers: HeadersInit = {\n 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n 'Access-Control-Allow-Headers': 'Content-Type, Authorization',\n };\n\n if (allowedOrigins && allowedOrigins.length > 0) {\n headers['Access-Control-Allow-Origin'] = allowedOrigins.join(', ');\n } else {\n headers['Access-Control-Allow-Origin'] = '*';\n }\n\n return headers;\n}\n\n/**\n * Helper to handle OPTIONS requests for CORS\n */\nexport function handleOptions(allowedOrigins?: string[]): NextResponse {\n return new NextResponse(null, {\n status: 200,\n headers: createCorsHeaders(allowedOrigins)\n });\n}","/**\n * Server-side reCAPTCHA verification utility\n */\n\nexport interface RecaptchaConfig {\n secretKey: string;\n minimumScore?: number; // For reCAPTCHA v3, minimum score (0.0 to 1.0)\n timeoutMs?: number; // Request timeout in milliseconds\n}\n\nexport interface RecaptchaVerificationResult {\n success: boolean;\n score?: number; // reCAPTCHA v3 score\n action?: string; // reCAPTCHA v3 action\n challengeTimestamp?: string;\n hostname?: string;\n errorCodes?: string[];\n}\n\n/**\n * Verify reCAPTCHA token with Google's API\n */\nexport async function verifyRecaptcha(\n token: string,\n config: RecaptchaConfig\n): Promise<RecaptchaVerificationResult> {\n if (!config.secretKey) {\n console.warn('RECAPTCHA_SECRET_KEY not configured, skipping verification');\n return { success: true };\n }\n\n if (!token || typeof token !== 'string') {\n return {\n success: false,\n errorCodes: ['missing-input-response']\n };\n }\n\n const timeout = config.timeoutMs || 10000; // 10 second default timeout\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n secret: config.secretKey,\n response: token\n }),\n signal: controller.signal\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n console.error('reCAPTCHA API returned non-OK status:', response.status);\n return {\n success: false,\n errorCodes: ['recaptcha-api-error']\n };\n }\n\n const data = await response.json();\n \n // Check minimum score for v3 (if configured)\n if (config.minimumScore !== undefined && data.score !== undefined) {\n if (data.score < config.minimumScore) {\n return {\n success: false,\n score: data.score,\n errorCodes: ['score-threshold-not-met']\n };\n }\n }\n\n return {\n success: data.success === true,\n score: data.score,\n action: data.action,\n challengeTimestamp: data.challenge_ts,\n hostname: data.hostname,\n errorCodes: data['error-codes'] || []\n };\n\n } catch (error) {\n if (error instanceof Error && error.name === 'AbortError') {\n console.error('reCAPTCHA verification timeout');\n return {\n success: false,\n errorCodes: ['timeout-or-duplicate']\n };\n }\n\n console.error('reCAPTCHA verification error:', error);\n return {\n success: false,\n errorCodes: ['network-error']\n };\n }\n}\n\n/**\n * Get human-readable error message for reCAPTCHA error codes\n */\nexport function getRecaptchaErrorMessage(errorCodes: string[]): string {\n const errorMessages: { [key: string]: string } = {\n 'missing-input-secret': 'reCAPTCHA secret key is missing',\n 'invalid-input-secret': 'reCAPTCHA secret key is invalid',\n 'missing-input-response': 'reCAPTCHA token is missing',\n 'invalid-input-response': 'reCAPTCHA token is invalid or malformed',\n 'bad-request': 'The request is invalid or malformed',\n 'timeout-or-duplicate': 'reCAPTCHA verification timed out or token was already used',\n 'score-threshold-not-met': 'reCAPTCHA score is below the required threshold',\n 'recaptcha-api-error': 'reCAPTCHA service is unavailable',\n 'network-error': 'Network error during reCAPTCHA verification'\n };\n\n if (!errorCodes || errorCodes.length === 0) {\n return 'reCAPTCHA verification failed';\n }\n\n const knownErrors = errorCodes\n .filter(code => errorMessages[code])\n .map(code => errorMessages[code]);\n\n return knownErrors.length > 0 \n ? knownErrors.join(', ')\n : 'reCAPTCHA verification failed';\n}","import { createReviewSubmitHandler, createReviewFetchHandler } from './handler';\nimport { ReviewHandlerConfig, ReviewFormData, ReviewApiResponse, Review } from '../types';\n\n/**\n * Configuration for BusinessFlow CRM review integration\n */\nexport interface BusinessFlowReviewConfig {\n /** BusinessFlow API base URL (e.g., https://api.businessflow.co.za) */\n apiUrl: string;\n /** BusinessFlow API key for authentication */\n apiKey: string;\n /** Source URL for review attribution (optional, will use NEXT_PUBLIC_SITE_URL if not provided) */\n sourceUrl?: string;\n /** reCAPTCHA secret key for server-side verification (optional) */\n recaptchaSecret?: string;\n /** Minimum reCAPTCHA score for v3 (optional, defaults to 0.5) */\n minimumScore?: number;\n /** Custom success callback after review is submitted to BusinessFlow */\n onSuccess?: (data: ReviewFormData, response: ReviewApiResponse) => Promise<void>;\n /** Custom error callback if submission fails */\n onError?: (data: ReviewFormData, error: Error) => Promise<void>;\n}\n\n/**\n * Create a NextJS API route handler that submits reviews to BusinessFlow CRM\n *\n * @example\n * ```typescript\n * // app/api/reviews/submit/route.ts\n * import { createBusinessFlowReviewHandler } from '@businessflow/reviews/server';\n *\n * export const POST = createBusinessFlowReviewHandler({\n * apiUrl: process.env.BUSINESS_FLOW_API_URL!,\n * apiKey: process.env.BUSINESS_FLOW_API_KEY!,\n * sourceUrl: process.env.SITE_URL!,\n * recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY!,\n * });\n * ```\n */\nexport function createBusinessFlowReviewHandler(config: BusinessFlowReviewConfig) {\n const handlerConfig: ReviewHandlerConfig = {\n onSubmit: async (data: ReviewFormData): Promise<ReviewApiResponse> => {\n // Remove token from data if present (already verified by reCAPTCHA handler)\n const { token, ...reviewData } = data;\n\n // Map fields to BusinessFlow API format\n const payload = {\n ReviewerName: reviewData.reviewerName,\n ReviewerEmail: reviewData.reviewerEmail,\n Rating: reviewData.rating,\n Content: reviewData.content || '',\n };\n\n try {\n const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Submit`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n body: JSON.stringify(payload),\n });\n\n const responseText = await response.text();\n let result;\n\n try {\n result = JSON.parse(responseText);\n } catch {\n // If response isn't JSON, create a response object\n result = {\n success: response.ok,\n message: response.ok ? 'Review submitted successfully' : responseText || 'Unknown error',\n };\n }\n\n if (!response.ok) {\n return {\n success: false,\n message: result.message || `HTTP ${response.status}: ${response.statusText}`,\n data: result,\n };\n }\n\n return {\n success: true,\n message: result.message || 'Review submitted successfully',\n reviewId: result.reviewId,\n status: result.status,\n data: result,\n };\n } catch (error) {\n console.error('BusinessFlow Review API error:', error);\n return {\n success: false,\n message: error instanceof Error ? error.message : 'Network error occurred',\n };\n }\n },\n\n // Configure reCAPTCHA if provided\n recaptcha: config.recaptchaSecret ? {\n secretKey: config.recaptchaSecret,\n minimumScore: config.minimumScore ?? 0.5,\n } : undefined,\n\n // Pass through callbacks\n onSuccess: config.onSuccess,\n onError: config.onError,\n };\n\n return createReviewSubmitHandler(handlerConfig);\n}\n\n/**\n * Create a NextJS API route handler that fetches featured reviews from BusinessFlow CRM\n *\n * @example\n * ```typescript\n * // app/api/reviews/featured/route.ts\n * import { createBusinessFlowFeaturedHandler } from '@businessflow/reviews/server';\n *\n * export const GET = createBusinessFlowFeaturedHandler({\n * apiUrl: process.env.BUSINESS_FLOW_API_URL!,\n * apiKey: process.env.BUSINESS_FLOW_API_KEY!,\n * });\n * ```\n */\nexport function createBusinessFlowFeaturedHandler(config: Pick<BusinessFlowReviewConfig, 'apiUrl' | 'apiKey'>) {\n return createReviewFetchHandler({\n onFetch: async (params?: any): Promise<Review[]> => {\n const limit = params?.limit || 10;\n\n try {\n const response = await fetch(`${config.apiUrl}/api/Marketing/Review/Featured?limit=${limit}`, {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n 'X-API-Key': config.apiKey,\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(`Failed to fetch featured reviews: ${errorText}`);\n }\n\n const data = await response.json();\n\n // Transform BusinessFlow response to Review interface\n return data.map((item: any) => ({\n id: `review_${Date.now()}_${Math.random()}`, // Generate unique ID\n reviewerName: item.reviewerName,\n reviewerEmail: '', // Not included in public response\n rating: item.rating,\n content: item.content,\n createdAt: item.submittedAt,\n featured: true,\n approved: true,\n }));\n } catch (error) {\n console.error('BusinessFlow Featured Reviews API error:', error);\n throw error;\n }\n },\n });\n}\n\n/**\n * Simplified BusinessFlow review handler for basic use cases\n * Uses environment variables for configuration\n *\n * @example\n * ```typescript\n * // app/api/reviews/submit/route.ts\n * import { createSimpleBusinessFlowReviewHandler } from '@businessflow/reviews/server';\n *\n * export const POST = createSimpleBusinessFlowReviewHandler();\n * ```\n *\n * Required environment variables:\n * - BUSINESS_FLOW_API_URL\n * - BUSINESS_FLOW_API_KEY\n * - RECAPTCHA_SECRET_KEY (optional)\n * - NEXT_PUBLIC_SITE_URL (optional, for sourceUrl)\n */\nexport function createSimpleBusinessFlowReviewHandler() {\n const apiUrl = process.env.BUSINESS_FLOW_API_URL;\n const apiKey = process.env.BUSINESS_FLOW_API_KEY;\n\n if (!apiUrl || !apiKey) {\n throw new Error(\n 'Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set'\n );\n }\n\n return createBusinessFlowReviewHandler({\n apiUrl,\n apiKey,\n sourceUrl: process.env.NEXT_PUBLIC_SITE_URL,\n recaptchaSecret: process.env.RECAPTCHA_SECRET_KEY,\n });\n}\n\n/**\n * Simplified BusinessFlow featured reviews handler for basic use cases\n * Uses environment variables for configuration\n *\n * @example\n * ```typescript\n * // app/api/reviews/featured/route.ts\n * import { createSimpleBusinessFlowFeaturedHandler } from '@businessflow/reviews/server';\n *\n * export const GET = createSimpleBusinessFlowFeaturedHandler();\n * ```\n */\nexport function createSimpleBusinessFlowFeaturedHandler() {\n const apiUrl = process.env.BUSINESS_FLOW_API_URL;\n const apiKey = process.env.BUSINESS_FLOW_API_KEY;\n\n if (!apiUrl || !apiKey) {\n throw new Error(\n 'Missing required environment variables: BUSINESS_FLOW_API_URL and BUSINESS_FLOW_API_KEY must be set'\n );\n }\n\n return createBusinessFlowFeaturedHandler({\n apiUrl,\n apiKey,\n });\n}"],"mappings":"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","names":[]}
|