From a2de135b11517273a89604e55766201240180972 Mon Sep 17 00:00:00 2001
From: Arthur Belleville
Date: Wed, 15 Oct 2025 16:30:36 +0200
Subject: [PATCH] Fix invite
---
api/src/tablo.ts | 85 ++++++++++++++++++++------------
api/src/transporter.ts | 4 +-
ui/src/components/TabloModal.tsx | 24 +++++----
ui/src/hooks/invite.ts | 12 +++--
4 files changed, 80 insertions(+), 45 deletions(-)
diff --git a/api/src/tablo.ts b/api/src/tablo.ts
index ade1b99..3377bb0 100644
--- a/api/src/tablo.ts
+++ b/api/src/tablo.ts
@@ -1,15 +1,27 @@
import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3";
-import { PostgrestError, type SupabaseClient, type User } from "@supabase/supabase-js";
+import {
+ PostgrestError,
+ type SupabaseClient,
+ type User,
+} from "@supabase/supabase-js";
import { Hono } from "hono";
import type { Transporter } from "nodemailer";
import type { StreamChat } from "stream-chat";
import { config } from "./config.js";
import type { Tables } from "./database.types.ts";
import { generateICSFromEvents, writeCalendarFileToR2 } from "./helpers.js";
-import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
+import {
+ authMiddleware,
+ r2Middleware,
+ streamChatMiddleware,
+} from "./middleware.js";
import { generateToken } from "./token.js";
import { transporter } from "./transporter.js";
-import type { EventAndTablo, EventInsertInTablo, TabloInsert } from "./types.ts";
+import type {
+ EventAndTablo,
+ EventInsertInTablo,
+ TabloInsert,
+} from "./types.ts";
export const tabloRouter = new Hono<{
Variables: {
@@ -166,7 +178,9 @@ tabloRouter.post("/create-and-invite", async (c) => {
const { data: insertedTablo, error } = await supabase
.from("tablos")
.insert({
- name: `${invitedUserDataTyped.name || "Invité"} / ${ownerDataTyped.name || "Propriétaire"}`,
+ name: `${invitedUserDataTyped.name || "Invité"} / ${
+ ownerDataTyped.name || "Propriétaire"
+ }`,
color: "bg-blue-500",
status: "todo",
owner_id: ownerId,
@@ -184,20 +198,22 @@ tabloRouter.post("/create-and-invite", async (c) => {
}
// Grant access to the current user (invited user) as a non-admin member
- const { error: tabloAccessError } = await supabase.from("tablo_access").insert(
- {
- tablo_id: tabloData.id,
- user_id: user.id,
- // ** IMPORTANT **
- is_admin: false,
- // -------------
- is_active: true,
- granted_by: ownerId,
- }
- // {
- // onConflict: "tablo_id, user_id",
- // }
- );
+ const { error: tabloAccessError } = await supabase
+ .from("tablo_access")
+ .insert(
+ {
+ tablo_id: tabloData.id,
+ user_id: user.id,
+ // ** IMPORTANT **
+ is_admin: false,
+ // -------------
+ is_active: true,
+ granted_by: ownerId,
+ }
+ // {
+ // onConflict: "tablo_id, user_id",
+ // }
+ );
if (tabloAccessError) {
console.error("tabloAccessError", tabloAccessError);
@@ -290,7 +306,8 @@ tabloRouter.patch("/update", async (c) => {
const updatedTablo = update as Tables<"tablos">;
- const isUpdatingName = tablo.name !== undefined && tablo.name !== updatedTablo.name;
+ const isUpdatingName =
+ tablo.name !== undefined && tablo.name !== updatedTablo.name;
if (error) {
return c.json({ error: error.message }, 500);
@@ -344,7 +361,6 @@ tabloRouter.delete("/delete", async (c) => {
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();
@@ -366,7 +382,10 @@ tabloRouter.post("/invite", async (c) => {
}
if (tablo.owner_id !== sender.id) {
- return c.json({ error: "You are not allowed to invite users to this tablo" }, 400);
+ return c.json(
+ { error: "You are not allowed to invite users to this tablo" },
+ 400
+ );
}
const { error } = await supabase.from("tablo_invites").insert({
@@ -386,7 +405,9 @@ tabloRouter.post("/invite", async (c) => {
subject: "Vous avez été invité à un tablo",
html: `Vous avez été invité à un tablo avec ce lien
`,
+ }/join/${encodeURIComponent(tablo.name)}?token=${encodeURIComponent(
+ token
+ )}">ce lien
`,
});
return c.json({
@@ -419,15 +440,17 @@ tabloRouter.post("/join", async (c) => {
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,
- });
+ 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);
diff --git a/api/src/transporter.ts b/api/src/transporter.ts
index 87357cf..42df093 100644
--- a/api/src/transporter.ts
+++ b/api/src/transporter.ts
@@ -4,7 +4,7 @@ import { config } from "./config.js";
const OAuth2 = google.auth.OAuth2;
-export const createTransporter = async () => {
+export const createTransporter = () => {
const oauth2Client = new OAuth2(
config.EMAIL_CLIENT_ID,
config.EMAIL_CLIENT_SECRET,
@@ -31,4 +31,4 @@ export const createTransporter = async () => {
return transporter;
};
-export const transporter = await createTransporter();
+export const transporter = createTransporter();
diff --git a/ui/src/components/TabloModal.tsx b/ui/src/components/TabloModal.tsx
index 2790bf8..b56551a 100644
--- a/ui/src/components/TabloModal.tsx
+++ b/ui/src/components/TabloModal.tsx
@@ -40,7 +40,7 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
const [showMembers, setShowMembers] = useState(false);
const [inviteEmail, setInviteEmail] = useState("");
- const inviteUser = useInviteUser();
+ const { mutate: inviteUser, isPending: isInvitingUser } = useInviteUser();
const {
data: fileData,
@@ -425,14 +425,20 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
placeholder="Email de l'utilisateur à inviter"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
/>
-
+ {isInvitingUser ? (
+
+ ) : (
+
+ )}
diff --git a/ui/src/hooks/invite.ts b/ui/src/hooks/invite.ts
index 2fbf268..b3d0f12 100644
--- a/ui/src/hooks/invite.ts
+++ b/ui/src/hooks/invite.ts
@@ -6,8 +6,14 @@ import { toast } from "@ui/ui-library/toast/toast-queue";
// Invite user by email
export const useInviteUser = () => {
const { session } = useSession();
- const { mutate } = useMutation({
- mutationFn: async ({ email, tablo_id }: { email: string; tablo_id: string }) => {
+ const { mutate, isPending } = useMutation({
+ mutationFn: async ({
+ email,
+ tablo_id,
+ }: {
+ email: string;
+ tablo_id: string;
+ }) => {
const { data } = await api.post(
"/api/v1/tablos/invite",
{
@@ -35,7 +41,7 @@ export const useInviteUser = () => {
);
},
});
- return mutate;
+ return { mutate, isPending };
};
export const useJoinTablo = () => {