import type { RequestHandler } from './$types'; import { getDb } from '$lib/server/db'; import { sha256Hex } from '$lib/server/crypto'; import { icsHorizonMonths } from '$lib/server/env'; import { buildIcsCalendar } from '$lib/server/ics'; function toIsoSql(d = new Date()): string { return d.toISOString().replace('T', ' ').replace('Z', ''); } function ymdUTC(date: Date): string { const yyyy = String(date.getUTCFullYear()).padStart(4, '0'); const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); const dd = String(date.getUTCDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}`; } function addMonthsUTC(date: Date, months: number): Date { const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); d.setUTCMonth(d.getUTCMonth() + months); return d; } export const GET: RequestHandler = async ({ params, request }) => { const db = await getDb(); const token = params.token || ''; if (!token) return new Response('Not Found', { status: 404 }); const tokenHash = await sha256Hex(token); const row = db .prepare( `SELECT id, type, user_id, group_id, revoked_at FROM calendar_tokens WHERE token_hash = ? LIMIT 1` ) .get(tokenHash) as any; if (!row) return new Response('Not Found', { status: 404 }); if (row.revoked_at) return new Response('Gone', { status: 410 }); if (String(row.type) !== 'group' || !row.group_id) return new Response('Not Found', { status: 404 }); // Validar estado del grupo (activo y no archivado); en caso contrario, tratar como feed caducado const gRow = db .prepare(`SELECT COALESCE(active,1) as active, COALESCE(archived,0) as archived FROM groups WHERE id = ?`) .get(row.group_id) as any; if (!gRow || Number(gRow.active || 0) !== 1 || Number(gRow.archived || 0) === 1) { db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); return new Response('Gone', { status: 410 }); } const today = new Date(); const startYmd = ymdUTC(today); const endYmd = ymdUTC(addMonthsUTC(today, icsHorizonMonths)); const tasks = db .prepare( `SELECT t.id, t.description, t.due_date, g.name AS group_name FROM tasks t LEFT JOIN groups g ON g.id = t.group_id WHERE t.group_id = ? AND COALESCE(t.completed, 0) = 0 AND t.due_date IS NOT NULL AND t.due_date >= ? AND t.due_date <= ? AND NOT EXISTS (SELECT 1 FROM task_assignments a WHERE a.task_id = t.id) ORDER BY t.due_date ASC, t.id ASC` ) .all(row.group_id, startYmd, endYmd) as Array<{ id: number; description: string; due_date: string; group_name: string | null }>; const events = tasks.map((t) => ({ id: t.id, description: t.description, due_date: t.due_date, group_name: t.group_name || null, prefix: 'T' })); const { body, etag } = await buildIcsCalendar('Tareas sin responsable (grupo)', events); // 304 si ETag coincide const inm = request.headers.get('if-none-match'); if (inm && inm === etag) { // Actualizar last_used_at aunque sea 304 db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); return new Response(null, { status: 304, headers: { ETag: etag, 'Cache-Control': 'public, max-age=300' } }); } db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); return new Response(body, { status: 200, headers: { 'content-type': 'text/calendar; charset=utf-8', 'cache-control': 'public, max-age=300', ETag: etag } }); };