277 lines
6.8 KiB
TypeScript
277 lines
6.8 KiB
TypeScript
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<TabloInsert, "owner_id">;
|
|
|
|
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 <noreply@xtablo.com>`,
|
|
to: recipientmail,
|
|
subject: "Vous avez été invité à un tablo",
|
|
html: `<p>Vous avez été invité à un tablo avec <a href="${
|
|
config.XTABLO_URL
|
|
}/join/${encodeURIComponent(tablo.name)}?token=${encodeURIComponent(
|
|
token
|
|
)}">ce lien</a></p>`,
|
|
});
|
|
|
|
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,
|
|
})),
|
|
});
|
|
});
|