Validaciones mandatorias — Sección 8 — IDs concretos
This commit is contained in:
109
app/actions/submit-survey.ts
Normal file
109
app/actions/submit-survey.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
'use server'
|
||||
import { headers } from 'next/headers'
|
||||
import { createHash, randomUUID } from 'crypto'
|
||||
import { createClient as createSupabaseClient } from '@supabase/supabase-js'
|
||||
|
||||
export type SubmitResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string }
|
||||
|
||||
export async function submitSurvey(payload: {
|
||||
surveyId: string
|
||||
answers: Record<string, unknown>
|
||||
questionMap: Record<string, string> // código → question_id UUID
|
||||
consentOperativo: boolean
|
||||
consentMacroN1: boolean
|
||||
}): Promise<SubmitResult> {
|
||||
// 1. ip_hash: server-only, nunca en texto plano
|
||||
const headersList = await headers()
|
||||
const rawIp =
|
||||
headersList.get('x-forwarded-for')?.split(',')[0]?.trim() ?? 'unknown'
|
||||
const salt = process.env.IP_HASH_SALT ?? ''
|
||||
const ipHash = createHash('sha256').update(rawIp + salt).digest('hex')
|
||||
|
||||
// 2. Cliente Supabase con anon key (RLS policies permiten INSERT a anon)
|
||||
// NUNCA usar service_role — el anon key respeta RLS
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL ?? ''
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ''
|
||||
const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey)
|
||||
|
||||
// 3. Leer survey para obtener org_id y consent_version_id
|
||||
const { data: survey, error: sErr } = await supabase
|
||||
.from('surveys')
|
||||
.select('org_id, consent_version_id')
|
||||
.eq('id', payload.surveyId)
|
||||
.single()
|
||||
|
||||
if (sErr || !survey) return { success: false, error: 'survey_not_found' }
|
||||
|
||||
// 4. Pre-generar UUIDs server-side para evitar .select().single() post-INSERT.
|
||||
//
|
||||
// DIAGNÓSTICO: el Supabase JS client usa RETURNING * en .select().single(), lo que
|
||||
// requiere SELECT policy para anon en consent_records/responses. Esa policy no existe
|
||||
// (correcto — anon no debe leer consent_records de otros respondentes).
|
||||
// Solución: pre-generar UUIDs con randomUUID() y NO hacer SELECT after INSERT.
|
||||
const consentId = randomUUID()
|
||||
const responseId = randomUUID()
|
||||
|
||||
// 5. INSERT consent_record (inmutable, ADR-003)
|
||||
// Sin .select() para no activar RETURNING y evitar el error RLS 42501.
|
||||
const { error: cErr } = await supabase
|
||||
.from('consent_records')
|
||||
.insert({
|
||||
id: consentId,
|
||||
consent_version_id: survey.consent_version_id,
|
||||
granted_scopes: {
|
||||
operativo: payload.consentOperativo,
|
||||
macro_n1: payload.consentMacroN1,
|
||||
},
|
||||
granted_at: new Date().toISOString(),
|
||||
})
|
||||
|
||||
if (cErr) return { success: false, error: `consent_insert_failed: ${cErr.message}` }
|
||||
|
||||
// 6. Extraer qi_* de las respuestas
|
||||
const qi_age_bucket =
|
||||
typeof payload.answers['A5'] === 'string' ? payload.answers['A5'] : null
|
||||
const qi_sector =
|
||||
typeof payload.answers['A4'] === 'string' ? payload.answers['A4'] : null
|
||||
|
||||
// 7. INSERT response (respondent_id=null — encuesta anónima, regla 2 CLAUDE.md)
|
||||
const { error: rErr } = await supabase
|
||||
.from('responses')
|
||||
.insert({
|
||||
id: responseId,
|
||||
survey_id: payload.surveyId,
|
||||
company_id: survey.org_id,
|
||||
respondent_id: null,
|
||||
ip_hash: ipHash,
|
||||
consent_record_id: consentId,
|
||||
qi_age_bucket: qi_age_bucket || null,
|
||||
qi_sector: qi_sector || null,
|
||||
qi_zone: null,
|
||||
})
|
||||
|
||||
if (rErr) return { success: false, error: `response_insert_failed: ${rErr.message}` }
|
||||
|
||||
// 8. INSERT answers — una fila por pregunta respondida
|
||||
const answerRows = Object.entries(payload.answers)
|
||||
.map(([code, value]) => ({
|
||||
response_id: responseId,
|
||||
question_id: payload.questionMap[code],
|
||||
value,
|
||||
}))
|
||||
.filter(
|
||||
(row) =>
|
||||
row.question_id &&
|
||||
row.value !== null &&
|
||||
row.value !== undefined &&
|
||||
row.value !== '' &&
|
||||
!(Array.isArray(row.value) && row.value.length === 0)
|
||||
)
|
||||
|
||||
if (answerRows.length > 0) {
|
||||
const { error: aErr } = await supabase.from('answers').insert(answerRows)
|
||||
if (aErr) return { success: false, error: `answers_insert_failed: ${aErr.message}` }
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
}
|
||||
Reference in New Issue
Block a user