import { Hono } from "hono"; import { authMiddleware, emailMiddleware, streamChatMiddleware, } from "./middleware.js"; import type { SupabaseClient, User } from "@supabase/supabase-js"; import type { Transporter } from "nodemailer"; import { generateToken } from "./token.js"; import { config } from "./config.js"; import type { Database, Tables } from "./database.types.js"; import type { StreamChat } from "stream-chat"; type Tablo = Database["public"]["Tables"]["tablos"]; type TabloInsert = Tablo["Insert"]; export const tabloRouter = new Hono<{ Variables: { user: User; supabase: SupabaseClient; transporter: Transporter; streamServerClient: StreamChat; }; }>(); tabloRouter.use(authMiddleware); tabloRouter.use(emailMiddleware); tabloRouter.use(streamChatMiddleware); tabloRouter.post("/create", async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); const data = await c.req.json(); const tablo = data as Omit; const { data: insertedTablo, error } = await supabase .from("tablos") .insert({ ...tablo, owner_id: user.id, }) .select() .single(); if (error) { return c.json({ error: error.message }, 500); } const tabloData = insertedTablo as Tables<"tablos">; const streamServerClient = c.get("streamServerClient"); const channel = streamServerClient.channel("messaging", tabloData.id, { // @ts-ignore name: tablo.name, created_by_id: user.id, members: [user.id], }); await channel.create(); return c.json({ message: "Tablo created successfully" }); }); tabloRouter.patch("/update", async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); const streamServerClient = c.get("streamServerClient"); const data = await c.req.json(); const { id, ...tablo } = data; const { data: update, error } = await supabase .from("tablos") .update(tablo) .eq("id", id) // TODO: this condition will need to be modified in the future .eq("owner_id", user.id) .select() .single(); const updatedTablo = update as Tables<"tablos">; if (error) { return c.json({ error: error.message }, 500); } const channel = streamServerClient.channel("messaging", updatedTablo.id); await channel.update({ // @ts-ignore name: updatedTablo.name, }); return c.json({ message: "Tablo updated successfully" }); }); tabloRouter.delete("/delete", async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); const streamServerClient = c.get("streamServerClient"); const data = await c.req.json(); const { id } = data; const { error } = await supabase .from("tablos") .update({ deleted_at: new Date().toISOString() }) .eq("id", id) .eq("owner_id", user.id); // TODO: verify in tablo access that the user is admin if (error) { return c.json({ error: error.message }, 500); } const channel = streamServerClient.channel("messaging", id); try { await channel.delete(); } catch (error) { console.error("error deleting channel", error); } return c.json({ message: "Tablo deleted successfully" }); }); tabloRouter.post("/invite", async (c) => { const sender = c.get("user"); const supabase = c.get("supabase"); const transporter = c.get("transporter"); const { email: recipientmail, tablo_id } = await c.req.json(); const token = generateToken(); const { data, error: tabloError } = await supabase .from("tablos") .select("*") .eq("id", tablo_id) .single(); const tablo = data as Tables<"tablos">; if (tabloError) { return c.json({ error: tabloError.message }, 500); } if (!tablo) { return c.json({ error: "Tablo not found" }, 404); } if (tablo.owner_id !== sender.id) { return c.json( { error: "You are not allowed to invite users to this tablo" }, 400 ); } const { error } = await supabase.from("tablo_invites").insert({ invited_email: recipientmail, tablo_id: tablo_id, invited_by: sender.id, invite_token: token, }); if (error) { return c.json({ error: error.message }, 500); } const info = await transporter.sendMail({ from: `${sender.email} via XTablo `, to: recipientmail, subject: "Vous avez été invité à un tablo", html: `

Vous avez été invité à un tablo avec ce lien

`, }); return c.json({ message: "Invite sent successfully", }); }); tabloRouter.post("/join", async (c) => { const { token } = await c.req.json(); const joiner = c.get("user"); const supabase = c.get("supabase"); const streamServerClient = c.get("streamServerClient"); const { data: inviteData, error } = await supabase .from("tablo_invites") .select("id, tablo_id, invited_by") .eq("invite_token", token) .eq("invited_email", joiner.email) .single(); if (error) { console.error("error", error); return c.json({ error: error.message }, 500); } if (!inviteData) { return c.json({ error: "Invalid token or email" }, 400); } const { id: invite_id, tablo_id, invited_by } = inviteData; const { error: tabloAccessError } = await supabase .from("tablo_access") .insert({ tablo_id, user_id: joiner.id, // ** IMPORTANT ** is_admin: false, // ------------- is_active: true, granted_by: invited_by, }); if (tabloAccessError) { console.error("tabloAccessError", tabloAccessError); return c.json({ error: tabloAccessError.message }, 500); } await supabase.from("tablo_invites").delete().eq("id", invite_id); const channel = streamServerClient.channel("messaging", tablo_id); await channel.addMembers([joiner.id]); return c.json({ message: "Tablo joined successfully" }); }); tabloRouter.get("/members/:tablo_id", async (c) => { const user = c.get("user"); const supabase = c.get("supabase"); const { tablo_id } = c.req.param(); const { data: tabloData, error: tabloError } = await supabase .from("user_tablos") .select("*") .eq("id", tablo_id) .eq("user_id", user.id); if (!tabloData || tabloData.length === 0) { return c.json({ error: "You are not a member of this tablo" }, 403); } if (tabloError) { return c.json({ error: "Internal server error" }, 500); } const { data, error } = await supabase .from("tablo_access") .select("is_admin, profiles(id, name)") .eq("tablo_id", tablo_id) .eq("is_active", true); const rows = data as unknown as { is_admin: boolean; profiles: { id: string; name: string; }; }[]; if (error) { return c.json({ error: error.message }, 500); } return c.json({ members: rows.map((member) => ({ ...member.profiles, is_admin: member.is_admin, })), }); });