From 75e72c1207ec1e6ee5fe9027c6dc0b58bf280a3c Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 3 Aug 2025 21:35:11 +0200 Subject: [PATCH] Add Supabase CA certificate, update TypeScript configuration, and implement calendar synchronization task --- api/.env.production | 15 ++++++++++++ api/.env.staging | 6 ++++- api/.gitignore | 3 +-- api/src/crontab | 1 + api/src/helpers.ts | 41 ++++++++++++++++++++++++++++++- api/src/index.ts | 7 +++--- api/src/middleware.ts | 2 +- api/src/routers.ts | 6 ++--- api/src/tablo.ts | 43 ++++++++++++--------------------- api/src/tasks/helloWorld.ts | 8 ------ api/src/tasks/syncCalendars.cjs | 34 ++++++++++++++++++++++++++ api/src/types.ts | 2 +- api/src/user.ts | 4 +-- api/{src => }/supabase_ca.crt | 0 api/tsconfig.json | 6 ++++- 15 files changed, 127 insertions(+), 51 deletions(-) create mode 100644 api/.env.production create mode 100644 api/src/crontab delete mode 100644 api/src/tasks/helloWorld.ts create mode 100644 api/src/tasks/syncCalendars.cjs rename api/{src => }/supabase_ca.crt (100%) diff --git a/api/.env.production b/api/.env.production new file mode 100644 index 0000000..0ad3fcd --- /dev/null +++ b/api/.env.production @@ -0,0 +1,15 @@ +SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co +SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTI0MTMyMSwiZXhwIjoyMDU2ODE3MzIxfQ.9r33CUsu6ZR4vyv4ed-UY6cLE1FZzSSxTNE8pFUKjN4 + +STREAM_CHAT_API_KEY=v4yf8rs94aa8 +STREAM_CHAT_API_SECRET=jq2szvv73ua7sz9tvr9y24dxg37sw8ue8t576fu7ggr4h6wvcmunby4gvte8tm8f + +EMAIL_USER="baptiste@xtablo.com" +EMAIL_KEY="jayf pzpj nrsv vtim" + +XTABLO_URL=https://app.xtablo.com +CORS_ORIGIN="https://app.xtablo.com" + +R2_ACCOUNT_ID="9715fa14c5e5d1612301572cf1c6bbee" +R2_ACCESS_KEY_ID="caeb987bbcd601708a93c6aa562064ef" +R2_SECRET_ACCESS_KEY="42e455b25804687f7cff3d15be23c1f0f47ca742d7a41b6fa1a05a91041e0215" \ No newline at end of file diff --git a/api/.env.staging b/api/.env.staging index dc5e54f..dd39b3f 100644 --- a/api/.env.staging +++ b/api/.env.staging @@ -8,4 +8,8 @@ EMAIL_USER="baptiste@xtablo.com" EMAIL_KEY="jayf pzpj nrsv vtim" XTABLO_URL="https://app-staging.xtablo.com" -CORS_ORIGIN="https://app-staging.xtablo.com" \ No newline at end of file +CORS_ORIGIN="https://app-staging.xtablo.com" + +R2_ACCOUNT_ID="9715fa14c5e5d1612301572cf1c6bbee" +R2_ACCESS_KEY_ID="caeb987bbcd601708a93c6aa562064ef" +R2_SECRET_ACCESS_KEY="42e455b25804687f7cff3d15be23c1f0f47ca742d7a41b6fa1a05a91041e0215" \ No newline at end of file diff --git a/api/.gitignore b/api/.gitignore index e319e06..cecfe13 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -16,8 +16,7 @@ node_modules/ .wrangler # env -.env -.env.production +.env.development .dev.vars # logs diff --git a/api/src/crontab b/api/src/crontab new file mode 100644 index 0000000..df8f6ba --- /dev/null +++ b/api/src/crontab @@ -0,0 +1 @@ +*/5 * * * * syncCalendars \ No newline at end of file diff --git a/api/src/helpers.ts b/api/src/helpers.ts index b7788fc..1804986 100644 --- a/api/src/helpers.ts +++ b/api/src/helpers.ts @@ -1,4 +1,6 @@ -import type { EventAndTablo } from "./types.js"; +import type { EventAndTablo } from "@/types.ts"; +import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import type { SupabaseClient } from "@supabase/supabase-js"; export const generateICSFromEvents = ( events: EventAndTablo[], @@ -67,3 +69,40 @@ export const generateICSFromEvents = ( icsContent += "\r\n" + "END: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, + }) + ); +}; diff --git a/api/src/index.ts b/api/src/index.ts index baebda6..cb91a10 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -1,10 +1,10 @@ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { logger } from "hono/logger"; -import { mainRouter } from "./routers.js"; +import { mainRouter } from "@/routers.ts"; import { cors } from "hono/cors"; -import { config } from "./config.js"; +import { config } from "@/config.ts"; import { run } from "graphile-worker"; import path from "path"; import { fileURLToPath } from "url"; @@ -39,7 +39,7 @@ app.route("/api/v1", mainRouter); const worker = async () => { const connectionString = `${ config.SUPABASE_CONNECTION_STRING - }?ssl=true&sslrootcert=${path.resolve(__dirname, "supabase_ca.crt")}`; + }?ssl=true&sslrootcert=${path.resolve(__dirname, "..", "supabase_ca.crt")}`; const runner = await run({ connectionString, @@ -47,6 +47,7 @@ const worker = async () => { pollInterval: 1000, taskDirectory: path.resolve(__dirname, "tasks"), noPreparedStatements: true, + crontabFile: path.resolve(__dirname, "crontab"), }); await runner.promise; diff --git a/api/src/middleware.ts b/api/src/middleware.ts index b2a5d78..6bc8007 100644 --- a/api/src/middleware.ts +++ b/api/src/middleware.ts @@ -3,7 +3,7 @@ import { createClient, type User } from "@supabase/supabase-js"; import type { Context, Next } from "hono"; import nodemailer from "nodemailer"; import { StreamChat } from "stream-chat"; -import { config } from "./config.js"; +import { config } from "@/config.ts"; // Create authentication middleware export const authMiddleware = async (c: Context, next: Next) => { diff --git a/api/src/routers.ts b/api/src/routers.ts index 80eab74..7a99aee 100644 --- a/api/src/routers.ts +++ b/api/src/routers.ts @@ -1,7 +1,7 @@ import { Hono } from "hono"; -import { userRouter } from "./user.js"; -import { supabaseMiddleware } from "./middleware.js"; -import { tabloRouter } from "./tablo.js"; +import { userRouter } from "@/user.ts"; +import { supabaseMiddleware } from "@/middleware.ts"; +import { tabloRouter } from "@/tablo.ts"; export const mainRouter = new Hono<{ Bindings: { diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 76fdf4f..0c38fb4 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -4,24 +4,24 @@ import { emailMiddleware, r2Middleware, streamChatMiddleware, -} from "./middleware.js"; +} from "@/middleware.ts"; import { PostgrestError, type SupabaseClient, type User, } from "@supabase/supabase-js"; import type { Transporter } from "nodemailer"; -import { generateToken } from "./token.js"; -import { config } from "./config.js"; -import type { Tables } from "./database.types.js"; +import { generateToken } from "@/token.ts"; +import { config } from "@/config.ts"; +import type { Tables } from "@/database.types.ts"; import type { StreamChat } from "stream-chat"; import type { TabloInsert, EventInsertInTablo, EventAndTablo, -} from "./types.js"; +} from "@/types.ts"; import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3"; -import { generateICSFromEvents } from "./helpers.js"; +import { generateICSFromEvents, writeCalendarFileToR2 } from "@/helpers.ts"; export const tabloRouter = new Hono<{ Variables: { @@ -399,31 +399,18 @@ tabloRouter.post("/webcal/generate-url", async (c) => { if (error) { return c.json({ error: "Failed to generate token" }, 500); } - 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) { - return c.json({ error: "Failed to generate events" }, 500); + try { + await writeCalendarFileToR2(s3_client, supabase, { + token, + tabloName, + tablo_id, + }); + } catch (error) { + console.error("error writing calendar file to R2", error); + return c.json({ error: "Failed to write calendar file to R2" }, 500); } - const eventsData = events as EventAndTablo[]; - - const icsContent = generateICSFromEvents(eventsData, tabloName); - - await s3_client.send( - new PutObjectCommand({ - Bucket: bucketName, - Key: key, - Body: icsContent, - }) - ); - // Return the webcal URL // const webcalUrl = `webcal://${ // c.req.header("host") || "localhost:3000" diff --git a/api/src/tasks/helloWorld.ts b/api/src/tasks/helloWorld.ts deleted file mode 100644 index 5358763..0000000 --- a/api/src/tasks/helloWorld.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { JobHelpers } from "graphile-worker"; - -export const helloWorld = async ( - payload: unknown, - helpers: JobHelpers -): Promise => { - helpers.logger.info("Hello World"); -}; diff --git a/api/src/tasks/syncCalendars.cjs b/api/src/tasks/syncCalendars.cjs new file mode 100644 index 0000000..428d70e --- /dev/null +++ b/api/src/tasks/syncCalendars.cjs @@ -0,0 +1,34 @@ +const { createClient } = require("@supabase/supabase-js"); +const { S3Client } = require("@aws-sdk/client-s3"); +const { config } = require("../config"); +const { writeCalendarFileToR2 } = require("../helpers"); + +module.exports = async (payload, helpers) => { + const supabase = createClient( + config.SUPABASE_URL, + config.SUPABASE_SERVICE_ROLE_KEY + ); + const s3 = new S3Client({ + region: "auto", + endpoint: `https://${config.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`, + credentials: { + accessKeyId: config.R2_ACCESS_KEY_ID, + secretAccessKey: config.R2_SECRET_ACCESS_KEY, + }, + }); + const { data, error } = await supabase.from("calendar_subscriptions").select("token, tablo_id, tablos(name)"); + if (error) { + helpers.logger.error(error); + } + + data.forEach(async (subscription) => { + const tabloName = subscription.tablos.name.replace(/ /g, "_"); + await writeCalendarFileToR2(s3, supabase, { + tabloName, + token: subscription.token, + tablo_id: subscription.tablo_id, + }); + }); + + helpers.logger.info("Synced calendars"); +}; \ No newline at end of file diff --git a/api/src/types.ts b/api/src/types.ts index 9710f6e..f752378 100644 --- a/api/src/types.ts +++ b/api/src/types.ts @@ -3,7 +3,7 @@ import type { Tables, TablesInsert, TablesUpdate, -} from "./database.types.js"; +} from "@/database.types.ts"; export type Tablo = Database["public"]["Tables"]["tablos"]; export type TabloInsert = Tablo["Insert"]; diff --git a/api/src/user.ts b/api/src/user.ts index 34028b2..d7aa652 100644 --- a/api/src/user.ts +++ b/api/src/user.ts @@ -1,9 +1,9 @@ import { Hono } from "hono"; -import { authMiddleware, streamChatMiddleware } from "./middleware.js"; +import { authMiddleware, streamChatMiddleware } from "@/middleware.ts"; import type { SupabaseClient, User } from "@supabase/supabase-js"; import { StreamChat } from "stream-chat"; import type { Transporter } from "nodemailer"; -import type { Tables } from "./database.types.js"; +import type { Tables } from "@/database.types.ts"; export const userRouter = new Hono<{ Variables: { diff --git a/api/src/supabase_ca.crt b/api/supabase_ca.crt similarity index 100% rename from api/src/supabase_ca.crt rename to api/supabase_ca.crt diff --git a/api/tsconfig.json b/api/tsconfig.json index 14d0a43..79cd88a 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -11,7 +11,11 @@ "outDir": "./dist", "declaration": true, "emitDeclarationOnly": true, - "allowImportingTsExtensions": true + "allowImportingTsExtensions": true, + "baseUrl": "./", + "paths": { + "@/*": ["./src/*"] + } }, "exclude": ["node_modules"] }