xtablo-source/api/src/helpers.ts
2025-10-28 12:01:03 +01:00

174 lines
4.9 KiB
TypeScript

import { ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
import type { SupabaseClient } from "@supabase/supabase-js";
import type { EventAndTablo } from "./types.ts";
import type { Context, Next } from "hono";
export const generateICSFromEvents = (
events: EventAndTablo[],
calendarName: string = "Planning"
) => {
const formatDate = (date: string, time: string) => {
// Combine date (YYYY-MM-DD) and time (HH:MM:SS) into ISO format then convert to UTC
const dateTime = new Date(`${date}T${time}`);
return `${dateTime.toISOString().replace(/[-:]/g, "").split(".")[0]}Z`;
};
const escapeICSText = (text: string) => {
return text
.replace(/\\/g, "\\\\")
.replace(/;/g, "\\;")
.replace(/,/g, "\\,")
.replace(/\n/g, "\\n")
.replace(/\r/g, "");
};
const generateUID = (eventId: string) => {
return `${eventId}@xtablo.com`;
};
let icsContent = [
"BEGIN:VCALENDAR",
"VERSION:2.0",
"PRODID:-//XTablo//Planning Export//EN",
`X-WR-CALNAME:${escapeICSText(calendarName)}`,
"X-WR-TIMEZONE:Europe/Paris",
"CALSCALE:GREGORIAN",
"METHOD:PUBLISH",
].join("\r\n");
events.forEach((event) => {
if (!event.start_date || !event.start_time || !event.title) return;
const startDateTime = formatDate(event.start_date, event.start_time);
const endDateTime = event.end_time
? formatDate(event.start_date, event.end_time)
: formatDate(event.start_date, event.start_time); // Default to start time if no end time
const eventLines = [
"",
"BEGIN:VEVENT",
`UID:${generateUID(event.event_id)}`,
`DTSTART:${startDateTime}`,
`DTEND:${endDateTime}`,
`SUMMARY:${escapeICSText(event.title)}`,
`DESCRIPTION:${escapeICSText(`Tablo: ${event.tablo_name}\n${event.description || ""}`)}`,
event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "",
`CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
`LAST-MODIFIED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
"STATUS:CONFIRMED",
"TRANSP:OPAQUE",
"END:VEVENT",
].filter((line) => line !== ""); // Remove empty lines
icsContent += `\r\n${eventLines.join("\r\n")}`;
});
icsContent += "\r\nEND:VCALENDAR";
return icsContent;
};
export const writeCalendarFileToR2 = async (
s3_client: S3Client,
supabase: SupabaseClient,
inputs: {
token: string;
tabloName: string;
tablo_id: string;
}
) => {
const { token, tabloName, tablo_id } = inputs;
const bucketName = "calendars";
const key = `${token}/${tabloName}.ics`;
const { data: events, error: eventsError } = await supabase
.from("events_and_tablos")
.select("*")
.eq("tablo_id", tablo_id);
if (eventsError || !events) {
throw new Error("Failed to generate events");
}
const eventsData = events as EventAndTablo[];
const icsContent = generateICSFromEvents(eventsData, tabloName);
await s3_client.send(
new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: icsContent,
})
);
};
export const getTabloFileNames = async (s3_client: S3Client, tabloId: string) => {
const bucketName = "tablo-data";
const { Contents } = await s3_client.send(
new ListObjectsV2Command({
Bucket: bucketName,
Prefix: tabloId,
})
);
return Contents?.map((content) => content.Key?.split("/")[1]).filter(
(content) => content?.length && content.length > 0
);
};
const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
const { data: tabloAccess, error: isMemberError } = await supabase
.from("tablo_access")
.select("*")
.eq("tablo_id", tabloId)
.eq("user_id", userId)
.eq("is_active", true);
if (isMemberError) {
return false;
}
return tabloAccess?.length > 0;
};
const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
const { data: tabloAccess, error: isAdminError } = await supabase
.from("tablo_access")
.select("*")
.eq("tablo_id", tabloId)
.eq("user_id", userId)
.eq("is_active", true)
.eq("is_admin", true);
if (isAdminError) {
return false;
}
return tabloAccess?.length > 0;
};
export const checkTabloMember = async (c: Context, next: Next) => {
const supabase = c.get("supabase");
const user = c.get("user");
const tabloId = c.req.param("tabloId");
const isMember = await isTabloMember(supabase, tabloId, user.id);
if (!isMember) {
return c.json({ error: "You are not a member of this tablo" }, 403);
}
await next();
};
export const checkTabloAdmin = async (c: Context, next: Next) => {
const supabase = c.get("supabase");
const user = c.get("user");
const tabloId = c.req.param("tabloId") || c.req.query("tablo_id");
const isAdmin = await isTabloAdmin(supabase, tabloId, user.id);
if (!isAdmin) {
return c.json({ error: "You are not an admin of this tablo" }, 403);
}
await next();
};