import type { RequestHandler } from './$types'; import { getDb } from '$lib/server/db'; function isValidYmd(input: string): boolean { const m = /^\s*(\d{4})-(\d{2})-(\d{2})\s*$/.exec(input || ''); if (!m) return false; const y = Number(m[1]), mo = Number(m[2]), d = Number(m[3]); if (!Number.isFinite(y) || !Number.isFinite(mo) || !Number.isFinite(d)) return false; if (mo < 1 || mo > 12 || d < 1 || d > 31) return false; const dt = new Date(`${String(y).padStart(4, '0')}-${String(mo).padStart(2, '0')}-${String(d).padStart(2, '0')}T00:00:00Z`); // Comprobar que el Date resultante coincide (evita 2025-02-31) return dt.getUTCFullYear() === y && (dt.getUTCMonth() + 1) === mo && dt.getUTCDate() === d; } export const PATCH: RequestHandler = async (event) => { const userId = event.locals.userId ?? null; if (!userId) { return new Response('Unauthorized', { status: 401 }); } const idStr = event.params.id || ''; const taskId = parseInt(idStr, 10); if (!Number.isFinite(taskId) || taskId <= 0) { return new Response('Bad Request', { status: 400 }); } let payload: any = null; try { payload = await event.request.json(); } catch { return new Response('Bad Request', { status: 400 }); } // Validar que al menos se envíe algún campo editable const hasDueField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'due_date'); const hasDescField = Object.prototype.hasOwnProperty.call(payload ?? {}, 'description'); if (!hasDueField && !hasDescField) { return new Response('Bad Request', { status: 400 }); } // due_date (opcional) const due_date_raw = payload?.due_date; if (hasDueField && due_date_raw !== null && due_date_raw !== undefined && typeof due_date_raw !== 'string') { return new Response('Bad Request', { status: 400 }); } const due_date = !hasDueField || due_date_raw == null || String(due_date_raw).trim() === '' ? null : String(due_date_raw).trim(); if (hasDueField && due_date !== null && !isValidYmd(due_date)) { return new Response(JSON.stringify({ error: 'invalid_due_date' }), { status: 400, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); } // description (opcional) let description: string | undefined = undefined; if (hasDescField) { const descRaw = payload?.description; if (descRaw !== null && descRaw !== undefined && typeof descRaw !== 'string') { return new Response('Bad Request', { status: 400 }); } if (descRaw == null) { // No permitimos null en description (columna NOT NULL) return new Response(JSON.stringify({ error: 'invalid_description' }), { status: 400, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); } const normalized = String(descRaw).replace(/\s+/g, ' ').trim(); if (normalized.length < 1 || normalized.length > 1000) { return new Response(JSON.stringify({ error: 'invalid_description' }), { status: 400, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); } description = normalized; } const db = await getDb(); // Cargar tarea y validar abierta const task = db .prepare( `SELECT id, description, due_date, group_id, created_by, COALESCE(completed, 0) AS completed, completed_at, display_code FROM tasks WHERE id = ?` ) .get(taskId) as any; if (!task) { return new Response(JSON.stringify({ status: 'not_found' }), { status: 404, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); } if (Number(task.completed) !== 0 || task.completed_at) { return new Response(JSON.stringify({ status: 'completed' }), { status: 400, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); } // Gating: grupo permitido + usuario miembro activo (si tiene group_id) const groupId: string | null = task.group_id ? String(task.group_id) : null; if (groupId) { const allowed = db .prepare(`SELECT 1 FROM allowed_groups WHERE group_id = ? AND status = 'allowed' LIMIT 1`) .get(groupId); const active = db .prepare( `SELECT 1 FROM group_members WHERE group_id = ? AND user_id = ? AND is_active = 1 LIMIT 1` ) .get(groupId, userId); const gstatus = db .prepare( `SELECT 1 FROM groups WHERE id = ? AND COALESCE(active,1)=1 AND COALESCE(archived,0)=0 LIMIT 1` ) .get(groupId); if (!allowed || !active || !gstatus) { return new Response('Forbidden', { status: 403 }); } } else { // Tarea sin grupo: permitir si el usuario está asignado o es el creador const isAssigned = db .prepare(`SELECT 1 FROM task_assignments WHERE task_id = ? AND user_id = ? LIMIT 1`) .get(taskId, userId); const isCreator = String(task.created_by || '') === String(userId); if (!isAssigned && !isCreator) { return new Response('Forbidden', { status: 403 }); } } // Aplicar actualización if (hasDescField && hasDueField) { db.prepare(`UPDATE tasks SET description = ?, due_date = ? WHERE id = ?`).run(description!, due_date, taskId); } else if (hasDescField) { db.prepare(`UPDATE tasks SET description = ? WHERE id = ?`).run(description!, taskId); } else if (hasDueField) { db.prepare(`UPDATE tasks SET due_date = ? WHERE id = ?`).run(due_date, taskId); } const updated = db .prepare(`SELECT id, description, due_date, display_code FROM tasks WHERE id = ?`) .get(taskId) as any; const body = { status: 'updated', task: { id: Number(updated.id), description: String(updated.description || ''), due_date: updated.due_date ? String(updated.due_date) : null, display_code: updated.display_code != null ? Number(updated.display_code) : null } }; return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' } }); };