Improve invites in api
This commit is contained in:
parent
d9a54be4c8
commit
2c2a41112c
4 changed files with 110 additions and 22 deletions
|
|
@ -353,6 +353,7 @@ export type Database = {
|
|||
id: string
|
||||
is_temporary: boolean
|
||||
last_name: string | null
|
||||
last_signed_in: string | null
|
||||
name: string | null
|
||||
short_user_id: string
|
||||
}
|
||||
|
|
@ -363,6 +364,7 @@ export type Database = {
|
|||
id: string
|
||||
is_temporary?: boolean
|
||||
last_name?: string | null
|
||||
last_signed_in?: string | null
|
||||
name?: string | null
|
||||
short_user_id: string
|
||||
}
|
||||
|
|
@ -373,6 +375,7 @@ export type Database = {
|
|||
id?: string
|
||||
is_temporary?: boolean
|
||||
last_name?: string | null
|
||||
last_signed_in?: string | null
|
||||
name?: string | null
|
||||
short_user_id?: string
|
||||
}
|
||||
|
|
@ -471,24 +474,30 @@ export type Database = {
|
|||
}
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
created_at: string
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
is_pending: boolean
|
||||
tablo_id: string
|
||||
}
|
||||
Insert: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
is_pending?: boolean
|
||||
tablo_id: string
|
||||
}
|
||||
Update: {
|
||||
created_at?: string
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
is_pending?: boolean
|
||||
tablo_id?: string
|
||||
}
|
||||
Relationships: [
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ export const checkTabloMember = async (c: Context, next: Next) => {
|
|||
export const checkTabloAdmin = async (c: Context, next: Next) => {
|
||||
const supabase = c.get("supabase");
|
||||
const user = c.get("user");
|
||||
const tabloId = c.req.param("tabloId");
|
||||
const tabloId = c.req.param("tabloId") || c.req.query("tablo_id");
|
||||
const isAdmin = await isTabloAdmin(supabase, tabloId, user.id);
|
||||
if (!isAdmin) {
|
||||
return c.json({ error: "You are not an admin of this tablo" }, 403);
|
||||
|
|
|
|||
115
api/src/tablo.ts
115
api/src/tablo.ts
|
|
@ -5,14 +5,14 @@ import type { Transporter } from "nodemailer";
|
|||
import type { StreamChat } from "stream-chat";
|
||||
import { config } from "./config.js";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import { writeCalendarFileToR2 } from "./helpers.js";
|
||||
import { checkTabloAdmin, writeCalendarFileToR2 } from "./helpers.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
regularUserCheckMiddleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { generateToken } from "./token.js";
|
||||
import { generatePassword, generateToken } from "./token.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
import type { EventInsertInTablo, TabloInsert } from "./types.ts";
|
||||
|
||||
|
|
@ -361,32 +361,24 @@ tabloRouter.delete("/delete", async (c) => {
|
|||
return c.json({ message: "Tablo deleted successfully" });
|
||||
});
|
||||
|
||||
tabloRouter.post("/invite", regularUserCheckMiddleware, async (c) => {
|
||||
tabloRouter.post("/invite/:tabloId", regularUserCheckMiddleware, checkTabloAdmin, async (c) => {
|
||||
const sender = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const { email: recipientmail, tablo_id } = await c.req.json();
|
||||
const { tabloId } = c.req.param();
|
||||
const { email: recipientmail } = await c.req.json();
|
||||
|
||||
const token = generateToken();
|
||||
|
||||
const { data, error: tabloError } = await supabase
|
||||
// Get tablo name
|
||||
const { data: tablo, error: tabloError } = await supabase
|
||||
.from("tablos")
|
||||
.select("*")
|
||||
.eq("id", tablo_id)
|
||||
.select("name")
|
||||
.eq("id", tabloId)
|
||||
.single();
|
||||
|
||||
const tablo = data as Tables<"tablos">;
|
||||
|
||||
if (tabloError) {
|
||||
return c.json({ error: tabloError.message }, 500);
|
||||
}
|
||||
|
||||
if (!tablo) {
|
||||
if (tabloError || !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 token = generateToken();
|
||||
|
||||
const { data: introConfigData, error: introError } = await supabase
|
||||
.from("user_introductions")
|
||||
|
|
@ -401,15 +393,94 @@ tabloRouter.post("/invite", regularUserCheckMiddleware, async (c) => {
|
|||
|
||||
const { error } = await supabase.from("tablo_invites").insert({
|
||||
invited_email: recipientmail,
|
||||
tablo_id: tablo_id,
|
||||
tablo_id: tabloId,
|
||||
invited_by: sender.id,
|
||||
invite_token: token,
|
||||
is_pending: true,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
// Get user from recipient email
|
||||
const { data: recipientUser, error: recipientError } = await supabase
|
||||
.from("profiles")
|
||||
.select("id")
|
||||
.eq("email", recipientmail)
|
||||
.maybeSingle();
|
||||
|
||||
if (recipientError) {
|
||||
return c.json({ error: recipientError.message }, 500);
|
||||
}
|
||||
|
||||
if (!recipientUser) {
|
||||
// Create a new invited user and add them to the tablo
|
||||
// Create a new user account for the invited email
|
||||
const password = generatePassword();
|
||||
const { data: newUser, error: createUserError } = await supabase.auth.admin.createUser({
|
||||
email: recipientmail,
|
||||
password: password,
|
||||
email_confirm: true,
|
||||
user_metadata: {
|
||||
name: recipientmail.split("@")[0],
|
||||
first_name: recipientmail,
|
||||
last_name: "",
|
||||
role: "invited_user",
|
||||
},
|
||||
app_metadata: {
|
||||
// Can't do that: https://github.com/supabase/auth/issues/1280
|
||||
// role: "invited_user",
|
||||
},
|
||||
});
|
||||
|
||||
if (createUserError) {
|
||||
return c.json({ error: createUserError.message }, 500);
|
||||
}
|
||||
|
||||
// Add the new user to the tablo
|
||||
const { error: accessError } = await supabase.from("tablo_access").insert({
|
||||
tablo_id: tabloId,
|
||||
user_id: newUser.user.id,
|
||||
granted_by: sender.id,
|
||||
is_active: true,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
});
|
||||
|
||||
if (accessError) {
|
||||
return c.json({ error: accessError.message }, 500);
|
||||
}
|
||||
|
||||
// Send welcome email to the new user
|
||||
await transporter.sendMail({
|
||||
from: `${sender.email} via XTablo <noreply@xtablo.com>`,
|
||||
to: recipientmail,
|
||||
subject: "Vous avez été invité sur XTablo",
|
||||
html: `
|
||||
<p>Bonjour !</p>
|
||||
|
||||
<p>${sender.email} vous a invité à rejoindre XTablo.</p>
|
||||
|
||||
<p>Un nouveau compte a été créé pour vous avec les identifiants suivants :</p>
|
||||
<div style="background-color: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 20px; margin: 16px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<p style="margin: 0; font-family: monospace;"><strong>Email :</strong> ${recipientmail}</p>
|
||||
<p style="margin: 8px 0 0 0; font-family: monospace;"><strong>Mot de passe :</strong> ${password}</p>
|
||||
</div>
|
||||
|
||||
<p>Veuillez cliquer sur le lien ci-dessous pour accepter l'invitation et configurer votre mot de passe.</p>
|
||||
|
||||
<p><a href="${config.XTABLO_URL}/login" style="display: inline-block; padding: 12px 24px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; font-weight: bold;">Accepter et se connecter</a></p>
|
||||
`,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "User created and invite sent successfully",
|
||||
});
|
||||
}
|
||||
|
||||
// Let the user know that they have been invited to the tablo
|
||||
await transporter.sendMail({
|
||||
from: `${sender.email} via XTablo <noreply@xtablo.com>`,
|
||||
to: recipientmail,
|
||||
|
|
@ -443,6 +514,7 @@ tabloRouter.post("/join", async (c) => {
|
|||
.select("id, tablo_id, invited_by")
|
||||
.eq("invite_token", token)
|
||||
.eq("invited_email", joiner.email)
|
||||
.eq("is_pending", true)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
|
|
@ -471,7 +543,8 @@ tabloRouter.post("/join", async (c) => {
|
|||
return c.json({ error: tabloAccessError.message }, 500);
|
||||
}
|
||||
|
||||
await supabase.from("tablo_invites").delete().eq("id", invite_id);
|
||||
// Mark invite as accepted instead of deleting (maintains audit trail)
|
||||
await supabase.from("tablo_invites").update({ is_pending: false }).eq("id", invite_id);
|
||||
|
||||
try {
|
||||
const channel = streamServerClient.channel("messaging", tablo_id);
|
||||
|
|
|
|||
|
|
@ -5,3 +5,9 @@ export const generateToken = (): string => {
|
|||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(36).padStart(2, "0")).join("");
|
||||
};
|
||||
|
||||
export const generatePassword = (): string => {
|
||||
const array = new Uint8Array(16);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (byte) => byte.toString(36).padStart(2, "0")).join("");
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue