You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
99 lines
3.5 KiB
TypeScript
99 lines
3.5 KiB
TypeScript
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
|
|
}
|
|
});
|
|
};
|