Add invite by email feature
This commit is contained in:
parent
6833089e0a
commit
2a1f0b7106
3 changed files with 116 additions and 33 deletions
|
|
@ -5,6 +5,7 @@ import { StreamChat } from "stream-chat";
|
|||
import type { Transporter } from "nodemailer";
|
||||
import { generateToken } from "./token.js";
|
||||
import { config } from "./config.js";
|
||||
import type { Tables } from "./database.types.js";
|
||||
|
||||
export const userRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
@ -44,12 +45,14 @@ userRouter.post("/invite", async (c) => {
|
|||
|
||||
const token = generateToken();
|
||||
|
||||
const { data: tablo, error: tabloError } = await supabase
|
||||
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);
|
||||
}
|
||||
|
|
@ -80,10 +83,53 @@ userRouter.post("/invite", async (c) => {
|
|||
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}/tablo/${tablo_id}?token=${token}">ce lien</a></p>`,
|
||||
html: `<p>Vous avez été invité à un tablo avec <a href="${config.XTABLO_URL}/join/${tablo.name}?token=${token}">ce lien</a></p>`,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "Invite sent successfully",
|
||||
});
|
||||
});
|
||||
|
||||
userRouter.post("/join-tablo", async (c) => {
|
||||
const { token } = await c.req.json();
|
||||
|
||||
const joiner = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("tablo_invites")
|
||||
.select("id, tablo_id, invited_by")
|
||||
.eq("invite_token", token)
|
||||
.eq("invited_email", joiner.email)
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return c.json({ error: "Invalid token or email" }, 400);
|
||||
}
|
||||
|
||||
const { id: invite_id, tablo_id, invited_by } = data;
|
||||
|
||||
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) {
|
||||
return c.json({ error: tabloAccessError.message }, 500);
|
||||
}
|
||||
|
||||
await supabase.from("tablo_invites").delete().eq("id", invite_id);
|
||||
return c.json({ message: "Tablo joined successfully" });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,3 +30,22 @@ export const useInviteUser = () => {
|
|||
});
|
||||
return mutate;
|
||||
};
|
||||
|
||||
export const useJoinTablo = () => {
|
||||
const { session } = useSession();
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async ({ token }: { token: string }) => {
|
||||
const { data } = await api.post(
|
||||
"/api/v1/users/join-tablo",
|
||||
{ token },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
return mutate;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { useJoinTablo } from "@ui/hooks/invite";
|
||||
|
||||
export const JoinPage = () => {
|
||||
const { tablo_name } = useParams<{ tablo_name: string }>();
|
||||
// const [searchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const user = useUser();
|
||||
const joinTablo = useJoinTablo();
|
||||
|
||||
// const token = searchParams.get("token");
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
|
||||
// const handleJoinTablo = async () => {
|
||||
// if (!user || !tablo_name || !token) return;
|
||||
|
|
@ -59,38 +62,40 @@ export const JoinPage = () => {
|
|||
// );
|
||||
// }
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center max-w-md">
|
||||
<h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Rejoindre Tablo
|
||||
</h1>
|
||||
<div className="bg-white p-6 rounded-lg shadow-lg mb-6">
|
||||
<h2 className="text-xl font-semibold mb-2">{tablo_name}</h2>
|
||||
<p className="text-gray-600 mb-4">
|
||||
Vous avez été invité(e) à rejoindre ce tablo
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-gray-600 mb-6">
|
||||
Veuillez vous connecter pour accepter cette invitation
|
||||
</p>
|
||||
<button
|
||||
onClick={() => navigate("/login")}
|
||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Se connecter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// if (!user) {
|
||||
// return (
|
||||
// <div className="min-h-screen flex items-center justify-center">
|
||||
// <div className="text-center max-w-md">
|
||||
// <h1 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
// Rejoindre Tablo "{tablo_name}"
|
||||
// </h1>
|
||||
// <div className="bg-white p-6 rounded-lg shadow-lg mb-6">
|
||||
// <h2 className="text-xl font-semibold mb-2">{tablo_name}</h2>
|
||||
// <p className="text-gray-600 mb-4">
|
||||
// Vous avez été invité(e) à rejoindre ce tablo
|
||||
// </p>
|
||||
// </div>
|
||||
// <p className="text-gray-600 mb-6">
|
||||
// Veuillez vous connecter pour accepter cette invitation
|
||||
// </p>
|
||||
// <button
|
||||
// onClick={() => navigate("/login")}
|
||||
// className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700"
|
||||
// >
|
||||
// Se connecter
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Rejoindre Tablo</h1>
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
Rejoindre Tablo "{tablo_name}"
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-8 rounded-lg shadow-lg">
|
||||
|
|
@ -99,13 +104,26 @@ export const JoinPage = () => {
|
|||
{tablo_name}
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
Vous avez été invité(e) à rejoindre ce tablo
|
||||
Vous avez été invité(e) par un enculé à rejoindre ce tablo
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => alert("Non implémenté")}
|
||||
onClick={() => {
|
||||
if (!user || !token) return;
|
||||
joinTablo(
|
||||
{ token },
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate("/");
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(error.message);
|
||||
},
|
||||
}
|
||||
);
|
||||
}}
|
||||
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Accepter l'invitation
|
||||
|
|
|
|||
Loading…
Reference in a new issue