Add Supabase CA certificate, update TypeScript configuration, and implement calendar synchronization task

This commit is contained in:
Arthur Belleville 2025-08-03 21:35:11 +02:00
parent 8d7252832f
commit 75e72c1207
No known key found for this signature in database
15 changed files with 127 additions and 51 deletions

15
api/.env.production Normal file
View file

@ -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"

View file

@ -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"
CORS_ORIGIN="https://app-staging.xtablo.com"
R2_ACCOUNT_ID="9715fa14c5e5d1612301572cf1c6bbee"
R2_ACCESS_KEY_ID="caeb987bbcd601708a93c6aa562064ef"
R2_SECRET_ACCESS_KEY="42e455b25804687f7cff3d15be23c1f0f47ca742d7a41b6fa1a05a91041e0215"

3
api/.gitignore vendored
View file

@ -16,8 +16,7 @@ node_modules/
.wrangler
# env
.env
.env.production
.env.development
.dev.vars
# logs

1
api/src/crontab Normal file
View file

@ -0,0 +1 @@
*/5 * * * * syncCalendars

View file

@ -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,
})
);
};

View file

@ -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;

View file

@ -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) => {

View file

@ -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: {

View file

@ -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"

View file

@ -1,8 +0,0 @@
import type { JobHelpers } from "graphile-worker";
export const helloWorld = async (
payload: unknown,
helpers: JobHelpers
): Promise<void> => {
helpers.logger.info("Hello World");
};

View file

@ -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");
};

View file

@ -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"];

View file

@ -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: {

View file

@ -11,7 +11,11 @@
"outDir": "./dist",
"declaration": true,
"emitDeclarationOnly": true,
"allowImportingTsExtensions": true
"allowImportingTsExtensions": true,
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules"]
}