Few improvements regarding tablos and channels sync
This commit is contained in:
parent
2bb2cb1f38
commit
991302eaaf
12 changed files with 783 additions and 530 deletions
|
|
@ -4,298 +4,298 @@ export type Json =
|
|||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
| Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string
|
||||
created_at: string
|
||||
date: string
|
||||
due_date: string
|
||||
id: string
|
||||
items: Json
|
||||
notes: string | null
|
||||
number: string
|
||||
status: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms: string | null
|
||||
total: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
client_email: string;
|
||||
created_at: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id: string;
|
||||
items: Json;
|
||||
notes: string | null;
|
||||
number: string;
|
||||
status: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms: string | null;
|
||||
total: number;
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
client_email: string
|
||||
created_at?: string
|
||||
date: string
|
||||
due_date: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms?: string | null
|
||||
total: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
client_email: string;
|
||||
created_at?: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms?: string | null;
|
||||
total: number;
|
||||
updated_at?: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
client_email?: string
|
||||
created_at?: string
|
||||
date?: string
|
||||
due_date?: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number?: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal?: number
|
||||
tax?: number
|
||||
terms?: string | null
|
||||
total?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
client_email?: string;
|
||||
created_at?: string;
|
||||
date?: string;
|
||||
due_date?: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number?: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal?: number;
|
||||
tax?: number;
|
||||
terms?: string | null;
|
||||
total?: number;
|
||||
updated_at?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
feedbacks: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
fd_type: string
|
||||
id: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
created_at: string | null;
|
||||
fd_type: string;
|
||||
id: number;
|
||||
message: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
fd_type: string
|
||||
id?: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
created_at?: string | null;
|
||||
fd_type: string;
|
||||
id?: number;
|
||||
message: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
fd_type?: string
|
||||
id?: number
|
||||
message?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
created_at?: string | null;
|
||||
fd_type?: string;
|
||||
id?: number;
|
||||
message?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
profiles: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
email: string | null
|
||||
id: string
|
||||
name: string | null
|
||||
}
|
||||
avatar_url: string | null;
|
||||
email: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id: string
|
||||
name?: string | null
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
};
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id?: string
|
||||
name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: number
|
||||
}
|
||||
id: number;
|
||||
invite_token: string;
|
||||
invited_by: string;
|
||||
invited_email: string;
|
||||
tablo_id: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: number
|
||||
}
|
||||
id?: number;
|
||||
invite_token: string;
|
||||
invited_by: string;
|
||||
invited_email: string;
|
||||
tablo_id: string;
|
||||
};
|
||||
Update: {
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
tablo_id?: number
|
||||
}
|
||||
id?: number;
|
||||
invite_token?: string;
|
||||
invited_by?: string;
|
||||
invited_email?: string;
|
||||
tablo_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id";
|
||||
columns: ["tablo_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "tablos";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
tablos: {
|
||||
Row: {
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: number
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
}
|
||||
color: string | null;
|
||||
created_at: string | null;
|
||||
deleted_at: string | null;
|
||||
id: string;
|
||||
image: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
position: number;
|
||||
status: string;
|
||||
};
|
||||
Insert: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: number
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
color?: string | null;
|
||||
created_at?: string | null;
|
||||
deleted_at?: string | null;
|
||||
id?: string;
|
||||
image?: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
position?: number;
|
||||
status?: string;
|
||||
};
|
||||
Update: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: number
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
color?: string | null;
|
||||
created_at?: string | null;
|
||||
deleted_at?: string | null;
|
||||
id?: string;
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
owner_id?: string;
|
||||
position?: number;
|
||||
status?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">];
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never;
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never;
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
|
|
@ -303,4 +303,4 @@ export const Constants = {
|
|||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { createClient, type User } from "@supabase/supabase-js";
|
||||
import type { Context, Next } from "hono";
|
||||
import nodemailer from "nodemailer";
|
||||
import { StreamChat } from "stream-chat";
|
||||
|
||||
// Create authentication middleware
|
||||
export const authMiddleware = async (c: Context, next: Next) => {
|
||||
|
|
@ -50,3 +51,15 @@ export const emailMiddleware = async (c: Context, next: Next) => {
|
|||
c.set("transporter", transporter);
|
||||
await next();
|
||||
};
|
||||
|
||||
export const streamChatMiddleware = async (c: Context, next: Next) => {
|
||||
const serverClient = StreamChat.getInstance(
|
||||
process.env.STREAM_CHAT_API_KEY as string,
|
||||
process.env.STREAM_CHAT_API_SECRET as string,
|
||||
{
|
||||
disableCache: true,
|
||||
}
|
||||
);
|
||||
c.set("streamServerClient", serverClient);
|
||||
await next();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Hono } from "hono";
|
||||
import { userRouter } from "./user.js";
|
||||
import { supabaseMiddleware } from "./middleware.js";
|
||||
import { tabloRouter } from "./tablo.js";
|
||||
|
||||
export const mainRouter = new Hono<{
|
||||
Bindings: {
|
||||
|
|
@ -28,3 +29,4 @@ mainRouter.use(supabaseMiddleware);
|
|||
// );
|
||||
|
||||
mainRouter.route("/users", userRouter);
|
||||
mainRouter.route("/tablos", tabloRouter);
|
||||
|
|
|
|||
154
api/src/tablo.ts
Normal file
154
api/src/tablo.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
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 { error } = await supabase.from("tablos").insert({
|
||||
...tablo,
|
||||
owner_id: user.id,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
const channel = streamServerClient.channel("messaging", tablo.name, {
|
||||
// @ts-ignore
|
||||
name: tablo.name,
|
||||
created_by_id: user.id,
|
||||
members: [user.id],
|
||||
});
|
||||
await channel.create();
|
||||
await channel.addMembers([user.id]);
|
||||
|
||||
return c.json({ message: "Tablo created 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/${tablo.name}?token=${token}">ce lien</a></p>`,
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "Invite sent successfully",
|
||||
});
|
||||
});
|
||||
|
||||
tabloRouter.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" });
|
||||
});
|
||||
131
api/src/user.ts
131
api/src/user.ts
|
|
@ -1,10 +1,8 @@
|
|||
import { Hono } from "hono";
|
||||
import { authMiddleware, emailMiddleware } from "./middleware.js";
|
||||
import { authMiddleware, streamChatMiddleware } from "./middleware.js";
|
||||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
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<{
|
||||
|
|
@ -12,124 +10,65 @@ export const userRouter = new Hono<{
|
|||
user: User;
|
||||
supabase: SupabaseClient;
|
||||
transporter: Transporter;
|
||||
streamServerClient: StreamChat;
|
||||
};
|
||||
}>();
|
||||
|
||||
userRouter.use(authMiddleware);
|
||||
userRouter.use(emailMiddleware);
|
||||
userRouter.use(streamChatMiddleware);
|
||||
|
||||
userRouter.get("/get-stream-token", async (c) => {
|
||||
const user = c.get("user");
|
||||
|
||||
const user_id = user.id;
|
||||
const serverClient = new StreamChat(
|
||||
process.env.STREAM_CHAT_API_KEY as string,
|
||||
process.env.STREAM_CHAT_API_SECRET as string,
|
||||
{
|
||||
disableCache: true,
|
||||
}
|
||||
);
|
||||
|
||||
const token = serverClient.createToken(user_id);
|
||||
|
||||
return c.json({
|
||||
token,
|
||||
});
|
||||
});
|
||||
|
||||
userRouter.post("/invite", async (c) => {
|
||||
const sender = c.get("user");
|
||||
userRouter.post("/sign-up-to-stream", async (c) => {
|
||||
const { id } = 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")
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", tablo_id)
|
||||
.eq("id", id)
|
||||
.single();
|
||||
|
||||
const tablo = data as Tables<"tablos">;
|
||||
const user = data as Tables<"profiles">;
|
||||
|
||||
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/${tablo.name}?token=${token}">ce lien</a></p>`,
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
await streamServerClient.upsertUser({
|
||||
id,
|
||||
name: user.name ?? "",
|
||||
language: "fr",
|
||||
});
|
||||
|
||||
return c.json({
|
||||
message: "Invite sent successfully",
|
||||
message: "User signed up to stream",
|
||||
});
|
||||
});
|
||||
|
||||
userRouter.post("/join-tablo", async (c) => {
|
||||
const { token } = await c.req.json();
|
||||
|
||||
const joiner = c.get("user");
|
||||
userRouter.get("/me", async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("tablo_invites")
|
||||
.select("id, tablo_id, invited_by")
|
||||
.eq("invite_token", token)
|
||||
.eq("invited_email", joiner.email)
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
|
||||
const userData = data as Tables<"profiles">;
|
||||
|
||||
if (!userData) {
|
||||
return c.json({ error: "User not found" }, 404);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return c.json({ error: "Invalid token or email" }, 400);
|
||||
}
|
||||
const user_id = data.id;
|
||||
const token = streamServerClient.createToken(user_id);
|
||||
|
||||
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" });
|
||||
return c.json({
|
||||
...userData,
|
||||
streamToken: token,
|
||||
});
|
||||
});
|
||||
|
||||
userRouter.post("");
|
||||
|
|
|
|||
|
|
@ -4,41 +4,41 @@
|
|||
|
||||
-- Sample tablos data
|
||||
INSERT INTO tablos (id, name, description, color, owner_id, is_public) VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Projet Alpha', 'Développement de la nouvelle application mobile', 'bg-blue-500', auth.uid(), false),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'Marketing Q4', 'Campagnes marketing pour le quatrième trimestre 2024', 'bg-green-500', auth.uid(), true),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'Équipe Dev', 'Coordination et suivi de l''équipe de développement', 'bg-purple-500', auth.uid(), false),
|
||||
('550e8400-e29b-41d4-a716-446655440004', 'Budget 2024', 'Planification et suivi budgétaire pour l''année 2024', 'bg-red-500', auth.uid(), false),
|
||||
('550e8400-e29b-41d4-a716-446655440005', 'Roadmap Produit', 'Feuille de route et évolution du produit', 'bg-yellow-500', auth.uid(), true),
|
||||
('550e8400-e29b-41d4-a716-446655440006', 'Support Client', 'Gestion et suivi du support client', 'bg-indigo-500', auth.uid(), false);
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'Projet Alpha', 'Développement de la nouvelle application mobile', 'bg-blue-500', auth.uid(), false),
|
||||
('M3N4O5P6Q7R8S9T0U1V2W3X4', 'Marketing Q4', 'Campagnes marketing pour le quatrième trimestre 2024', 'bg-green-500', auth.uid(), true),
|
||||
('Y5Z6A7B8C9D0E1F2G3H4I5J6', 'Équipe Dev', 'Coordination et suivi de l''équipe de développement', 'bg-purple-500', auth.uid(), false),
|
||||
('K7L8M9N0O1P2Q3R4S5T6U7V8', 'Budget 2024', 'Planification et suivi budgétaire pour l''année 2024', 'bg-red-500', auth.uid(), false),
|
||||
('W9X0Y1Z2A3B4C5D6E7F8G9H0', 'Roadmap Produit', 'Feuille de route et évolution du produit', 'bg-yellow-500', auth.uid(), true),
|
||||
('I1J2K3L4M5N6O7P8Q9R0S1T2', 'Support Client', 'Gestion et suivi du support client', 'bg-indigo-500', auth.uid(), false);
|
||||
|
||||
-- Sample boards for each tablo
|
||||
INSERT INTO tablo_boards (tablo_id, name, type, description, position, created_by) VALUES
|
||||
-- Projet Alpha boards
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Développement', 'kanban', 'Suivi des tâches de développement', 0, auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Planning', 'calendar', 'Calendrier du projet', 1, auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'Discussion', 'chat', 'Chat de l''équipe projet', 2, auth.uid()),
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'Développement', 'kanban', 'Suivi des tâches de développement', 0, auth.uid()),
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'Planning', 'calendar', 'Calendrier du projet', 1, auth.uid()),
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'Discussion', 'chat', 'Chat de l''équipe projet', 2, auth.uid()),
|
||||
|
||||
-- Marketing Q4 boards
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'Campagnes', 'kanban', 'Suivi des campagnes marketing', 0, auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'Calendrier Editorial', 'calendar', 'Planning des publications', 1, auth.uid()),
|
||||
('M3N4O5P6Q7R8S9T0U1V2W3X4', 'Campagnes', 'kanban', 'Suivi des campagnes marketing', 0, auth.uid()),
|
||||
('M3N4O5P6Q7R8S9T0U1V2W3X4', 'Calendrier Editorial', 'calendar', 'Planning des publications', 1, auth.uid()),
|
||||
|
||||
-- Équipe Dev boards
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'Sprint Board', 'kanban', 'Tableau de bord du sprint actuel', 0, auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'Backlog', 'table', 'Backlog produit', 1, auth.uid());
|
||||
('Y5Z6A7B8C9D0E1F2G3H4I5J6', 'Sprint Board', 'kanban', 'Tableau de bord du sprint actuel', 0, auth.uid()),
|
||||
('Y5Z6A7B8C9D0E1F2G3H4I5J6', 'Backlog', 'table', 'Backlog produit', 1, auth.uid());
|
||||
|
||||
-- Sample lists for Kanban boards
|
||||
INSERT INTO tablo_lists (board_id, name, position, color) VALUES
|
||||
-- For Projet Alpha - Développement board
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = '550e8400-e29b-41d4-a716-446655440001'), 'À faire', 0, 'bg-gray-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = '550e8400-e29b-41d4-a716-446655440001'), 'En cours', 1, 'bg-blue-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = '550e8400-e29b-41d4-a716-446655440001'), 'En test', 2, 'bg-yellow-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = '550e8400-e29b-41d4-a716-446655440001'), 'Terminé', 3, 'bg-green-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = 'A1B2C3D4E5F6G7H8I9J0K1L2'), 'À faire', 0, 'bg-gray-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = 'A1B2C3D4E5F6G7H8I9J0K1L2'), 'En cours', 1, 'bg-blue-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = 'A1B2C3D4E5F6G7H8I9J0K1L2'), 'En test', 2, 'bg-yellow-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Développement' AND tablo_id = 'A1B2C3D4E5F6G7H8I9J0K1L2'), 'Terminé', 3, 'bg-green-200'),
|
||||
|
||||
-- For Marketing Q4 - Campagnes board
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = '550e8400-e29b-41d4-a716-446655440002'), 'Idées', 0, 'bg-purple-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = '550e8400-e29b-41d4-a716-446655440002'), 'En préparation', 1, 'bg-orange-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = '550e8400-e29b-41d4-a716-446655440002'), 'En cours', 2, 'bg-blue-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = '550e8400-e29b-41d4-a716-446655440002'), 'Terminées', 3, 'bg-green-200');
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = 'M3N4O5P6Q7R8S9T0U1V2W3X4'), 'Idées', 0, 'bg-purple-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = 'M3N4O5P6Q7R8S9T0U1V2W3X4'), 'En préparation', 1, 'bg-orange-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = 'M3N4O5P6Q7R8S9T0U1V2W3X4'), 'En cours', 2, 'bg-blue-200'),
|
||||
((SELECT id FROM tablo_boards WHERE name = 'Campagnes' AND tablo_id = 'M3N4O5P6Q7R8S9T0U1V2W3X4'), 'Terminées', 3, 'bg-green-200');
|
||||
|
||||
-- Sample cards
|
||||
INSERT INTO tablo_cards (list_id, title, description, position, priority, due_date, created_by) VALUES
|
||||
|
|
@ -53,10 +53,10 @@ INSERT INTO tablo_cards (list_id, title, description, position, priority, due_da
|
|||
|
||||
-- Sample chat channels
|
||||
INSERT INTO tablo_chat_channels (tablo_id, name, type, description, created_by) VALUES
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'général', 'public', 'Discussion générale du projet Alpha', auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440001', 'dev-team', 'private', 'Canal privé pour l''équipe de développement', auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440002', 'marketing-general', 'public', 'Discussion générale marketing', auth.uid()),
|
||||
('550e8400-e29b-41d4-a716-446655440003', 'daily-standup', 'public', 'Daily standup de l''équipe dev', auth.uid());
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'général', 'public', 'Discussion générale du projet Alpha', auth.uid()),
|
||||
('A1B2C3D4E5F6G7H8I9J0K1L2', 'dev-team', 'private', 'Canal privé pour l''équipe de développement', auth.uid()),
|
||||
('M3N4O5P6Q7R8S9T0U1V2W3X4', 'marketing-general', 'public', 'Discussion générale marketing', auth.uid()),
|
||||
('Y5Z6A7B8C9D0E1F2G3H4I5J6', 'daily-standup', 'public', 'Daily standup de l''équipe dev', auth.uid());
|
||||
|
||||
-- Sample chat messages
|
||||
INSERT INTO tablo_chat_messages (channel_id, user_id, content, message_type) VALUES
|
||||
|
|
|
|||
115
sql/12_update_tablos_id_to_random_string.sql
Normal file
115
sql/12_update_tablos_id_to_random_string.sql
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
-- Migration: Update tablos table ID from SERIAL to random 24-character string
|
||||
-- This migration changes the tablos.id column from SERIAL to TEXT with random 24-character string IDs
|
||||
|
||||
-- Step 1: Create function to generate random 24-character strings
|
||||
CREATE OR REPLACE FUNCTION generate_random_string(length INTEGER DEFAULT 24)
|
||||
RETURNS TEXT
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
DECLARE
|
||||
chars TEXT := 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
result TEXT := '';
|
||||
i INTEGER := 0;
|
||||
BEGIN
|
||||
FOR i IN 1..length LOOP
|
||||
result := result || substr(chars, floor(random() * length(chars) + 1)::INTEGER, 1);
|
||||
END LOOP;
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Step 2: Drop existing foreign key constraints
|
||||
ALTER TABLE tablo_invites DROP CONSTRAINT IF EXISTS fk_tablo_invitations_tablo_id;
|
||||
ALTER TABLE tablo_access DROP CONSTRAINT IF EXISTS fk_tablo_access_tablo_id;
|
||||
|
||||
-- Step 3: Drop the trigger that creates tablo_access records
|
||||
DROP TRIGGER IF EXISTS trigger_create_tablo_access ON tablos;
|
||||
|
||||
-- Step 4: Create temporary columns for the new ID structure
|
||||
ALTER TABLE tablos ADD COLUMN new_id TEXT;
|
||||
ALTER TABLE tablo_invites ADD COLUMN new_tablo_id TEXT;
|
||||
ALTER TABLE tablo_access ADD COLUMN new_tablo_id TEXT;
|
||||
|
||||
-- Step 5: Generate new random IDs for existing tablos
|
||||
UPDATE tablos SET new_id = generate_random_string(24);
|
||||
|
||||
-- Step 6: Update foreign key references
|
||||
UPDATE tablo_invites SET new_tablo_id = (
|
||||
SELECT new_id FROM tablos WHERE tablos.id = tablo_invites.tablo_id
|
||||
);
|
||||
|
||||
UPDATE tablo_access SET new_tablo_id = (
|
||||
SELECT new_id FROM tablos WHERE tablos.id = tablo_access.tablo_id
|
||||
);
|
||||
|
||||
-- Step 7: Drop old columns
|
||||
ALTER TABLE tablos DROP COLUMN id;
|
||||
ALTER TABLE tablo_invites DROP COLUMN tablo_id;
|
||||
ALTER TABLE tablo_access DROP COLUMN tablo_id;
|
||||
|
||||
-- Step 8: Rename new columns to original names
|
||||
ALTER TABLE tablos RENAME COLUMN new_id TO id;
|
||||
ALTER TABLE tablo_invites RENAME COLUMN new_tablo_id TO tablo_id;
|
||||
ALTER TABLE tablo_access RENAME COLUMN new_tablo_id TO tablo_id;
|
||||
|
||||
-- Step 9: Add constraints and indexes
|
||||
ALTER TABLE tablos ADD PRIMARY KEY (id);
|
||||
ALTER TABLE tablos ALTER COLUMN id SET NOT NULL;
|
||||
ALTER TABLE tablos ALTER COLUMN id SET DEFAULT generate_random_string(24);
|
||||
|
||||
-- Step 10: Re-add foreign key constraints
|
||||
ALTER TABLE tablo_invites
|
||||
ADD CONSTRAINT fk_tablo_invitations_tablo_id
|
||||
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE tablo_access
|
||||
ADD CONSTRAINT fk_tablo_access_tablo_id
|
||||
FOREIGN KEY (tablo_id) REFERENCES tablos(id) ON DELETE CASCADE;
|
||||
|
||||
-- Step 11: Ensure NOT NULL constraints on foreign keys
|
||||
ALTER TABLE tablo_invites ALTER COLUMN tablo_id SET NOT NULL;
|
||||
ALTER TABLE tablo_access ALTER COLUMN tablo_id SET NOT NULL;
|
||||
|
||||
-- Step 12: Recreate the trigger function with updated signature
|
||||
CREATE OR REPLACE FUNCTION create_tablo_access_for_owner()
|
||||
RETURNS TRIGGER
|
||||
SECURITY DEFINER
|
||||
AS $$
|
||||
BEGIN
|
||||
-- Insert a tablo_access record for the tablo owner
|
||||
INSERT INTO tablo_access (
|
||||
tablo_id,
|
||||
user_id,
|
||||
granted_by,
|
||||
is_active,
|
||||
is_admin
|
||||
) VALUES (
|
||||
NEW.id, -- tablo_id: the newly created tablo's id (now TEXT)
|
||||
NEW.owner_id, -- user_id: the tablo owner gets access
|
||||
NEW.owner_id, -- granted_by: self-granted by the owner
|
||||
TRUE, -- is_active: access is active
|
||||
TRUE -- is_admin: owner has admin privileges
|
||||
);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Step 13: Recreate the trigger
|
||||
CREATE TRIGGER trigger_create_tablo_access
|
||||
AFTER INSERT ON tablos
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION create_tablo_access_for_owner();
|
||||
|
||||
-- Step 14: Add comment to document the changes
|
||||
COMMENT ON FUNCTION generate_random_string(INTEGER) IS
|
||||
'Generates a random alphanumeric string of specified length (default 24 characters)';
|
||||
|
||||
COMMENT ON COLUMN tablos.id IS
|
||||
'Primary key: random 24-character alphanumeric string';
|
||||
|
||||
COMMENT ON COLUMN tablo_invites.tablo_id IS
|
||||
'Foreign key reference to tablos.id (24-character string)';
|
||||
|
||||
COMMENT ON COLUMN tablo_access.tablo_id IS
|
||||
'Foreign key reference to tablos.id (24-character string)';
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
Session,
|
||||
createClient,
|
||||
} from "@supabase/supabase-js";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { api, queryClient } from "@ui/lib/api";
|
||||
|
||||
export type User = SupabaseUser & {
|
||||
user_metadata: {
|
||||
|
|
@ -51,6 +51,7 @@ interface AuthResponse {
|
|||
export function useSignUp() {
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
const { mutate, isPending } = useMutation<
|
||||
AuthResponse,
|
||||
{ message: string; code: string },
|
||||
|
|
@ -69,6 +70,9 @@ export function useSignUp() {
|
|||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
if (response.session?.access_token) {
|
||||
await signUpToStream(response.session.access_token);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -96,9 +100,27 @@ export function useSignUp() {
|
|||
return { mutate, isPending, errors };
|
||||
}
|
||||
|
||||
export function useSignUpToStream() {
|
||||
const { mutate: signUpToStream } = useMutation({
|
||||
mutationFn: async (accessToken: string) => {
|
||||
const { data } = await api.post<{ streamToken: string }>(
|
||||
"/api/v1/users/sign-up-to-stream",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
return { signUpToStream };
|
||||
}
|
||||
export function useLoginEmail() {
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
const { mutate, isPending } = useMutation<
|
||||
AuthResponse,
|
||||
{ message: string; code: string },
|
||||
|
|
@ -110,6 +132,9 @@ export function useLoginEmail() {
|
|||
password: data.password.trim(),
|
||||
});
|
||||
if (error) throw error;
|
||||
if (response.session?.access_token) {
|
||||
await signUpToStream(response.session.access_token);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|||
import { Database } from "@ui/types/database.types";
|
||||
import { supabase } from "./auth";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { toast } from "@ui/ui-library/toast/toast-queue";
|
||||
|
||||
type Tablo = Database["public"]["Tables"]["tablos"];
|
||||
|
||||
|
|
@ -25,7 +27,7 @@ export const useTablosList = () => {
|
|||
};
|
||||
|
||||
// Fetch single tablo
|
||||
export const useTablo = (id: number) => {
|
||||
export const useTablo = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ["tablos", id],
|
||||
queryFn: async () => {
|
||||
|
|
@ -47,15 +49,24 @@ export const useCreateTablo = () => {
|
|||
|
||||
return useMutation({
|
||||
mutationFn: async (tablo: Omit<TabloInsert, "owner_id">) => {
|
||||
const { error } = await supabase.from("tablos").insert({
|
||||
...tablo,
|
||||
owner_id: session?.user.id ?? "",
|
||||
const { data } = await api.post("/api/v1/tablos/create", tablo, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tablos"] });
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.add({
|
||||
title: "Échec de la création du tablo",
|
||||
description: "Veuillez réessayer",
|
||||
type: "error",
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { useSignUpToStream } from "@ui/hooks/auth";
|
||||
|
||||
export const OAuthSigninPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { session } = useSession();
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (session) {
|
||||
signUpToStream(session.access_token);
|
||||
navigate("/");
|
||||
}
|
||||
}, 100);
|
||||
return () => clearInterval(interval);
|
||||
}, [navigate, session]);
|
||||
}, [navigate, session, signUpToStream]);
|
||||
return <></>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { createStore, StoreApi, useStore } from "zustand";
|
||||
import React from "react";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Tables } from "@ui/types/database.types";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
|
|
@ -20,28 +19,20 @@ export const UserStoreProvider = ({
|
|||
}) => {
|
||||
const { session } = useSession();
|
||||
const shouldFetchUser = !!session?.access_token;
|
||||
const { data, isPending } = useQuery<User | null>({
|
||||
const { data: user, isPending } = useQuery<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.from("profiles").select("*");
|
||||
if (error) throw error;
|
||||
let token = null;
|
||||
try {
|
||||
const {
|
||||
data: { token: streamToken },
|
||||
} = await api.get("/api/v1/users/get-stream-token", {
|
||||
const { data: user } = await api.get<User>("/api/v1/users/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
token = streamToken;
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error("Failed to get stream token:", error);
|
||||
console.error("Failed to get user:", error);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...data[0],
|
||||
streamToken: token,
|
||||
};
|
||||
},
|
||||
enabled: shouldFetchUser,
|
||||
});
|
||||
|
|
@ -50,11 +41,11 @@ export const UserStoreProvider = ({
|
|||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
if (!user) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const store = createStore<User>()(() => data);
|
||||
const store = createStore<User>()(() => user);
|
||||
|
||||
return (
|
||||
<UserStoreContext.Provider value={store as StoreApi<User>}>
|
||||
|
|
|
|||
|
|
@ -4,298 +4,298 @@ export type Json =
|
|||
| boolean
|
||||
| null
|
||||
| { [key: string]: Json | undefined }
|
||||
| Json[]
|
||||
| Json[];
|
||||
|
||||
export type Database = {
|
||||
public: {
|
||||
Tables: {
|
||||
devis: {
|
||||
Row: {
|
||||
client_email: string
|
||||
created_at: string
|
||||
date: string
|
||||
due_date: string
|
||||
id: string
|
||||
items: Json
|
||||
notes: string | null
|
||||
number: string
|
||||
status: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms: string | null
|
||||
total: number
|
||||
updated_at: string
|
||||
user_id: string
|
||||
}
|
||||
client_email: string;
|
||||
created_at: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id: string;
|
||||
items: Json;
|
||||
notes: string | null;
|
||||
number: string;
|
||||
status: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms: string | null;
|
||||
total: number;
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
client_email: string
|
||||
created_at?: string
|
||||
date: string
|
||||
due_date: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal: number
|
||||
tax: number
|
||||
terms?: string | null
|
||||
total: number
|
||||
updated_at?: string
|
||||
user_id: string
|
||||
}
|
||||
client_email: string;
|
||||
created_at?: string;
|
||||
date: string;
|
||||
due_date: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal: number;
|
||||
tax: number;
|
||||
terms?: string | null;
|
||||
total: number;
|
||||
updated_at?: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
client_email?: string
|
||||
created_at?: string
|
||||
date?: string
|
||||
due_date?: string
|
||||
id?: string
|
||||
items?: Json
|
||||
notes?: string | null
|
||||
number?: string
|
||||
status?: Database["public"]["Enums"]["devis_status"]
|
||||
subtotal?: number
|
||||
tax?: number
|
||||
terms?: string | null
|
||||
total?: number
|
||||
updated_at?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
client_email?: string;
|
||||
created_at?: string;
|
||||
date?: string;
|
||||
due_date?: string;
|
||||
id?: string;
|
||||
items?: Json;
|
||||
notes?: string | null;
|
||||
number?: string;
|
||||
status?: Database["public"]["Enums"]["devis_status"];
|
||||
subtotal?: number;
|
||||
tax?: number;
|
||||
terms?: string | null;
|
||||
total?: number;
|
||||
updated_at?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
feedbacks: {
|
||||
Row: {
|
||||
created_at: string | null
|
||||
fd_type: string
|
||||
id: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
created_at: string | null;
|
||||
fd_type: string;
|
||||
id: number;
|
||||
message: string;
|
||||
user_id: string;
|
||||
};
|
||||
Insert: {
|
||||
created_at?: string | null
|
||||
fd_type: string
|
||||
id?: number
|
||||
message: string
|
||||
user_id: string
|
||||
}
|
||||
created_at?: string | null;
|
||||
fd_type: string;
|
||||
id?: number;
|
||||
message: string;
|
||||
user_id: string;
|
||||
};
|
||||
Update: {
|
||||
created_at?: string | null
|
||||
fd_type?: string
|
||||
id?: number
|
||||
message?: string
|
||||
user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
created_at?: string | null;
|
||||
fd_type?: string;
|
||||
id?: number;
|
||||
message?: string;
|
||||
user_id?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
profiles: {
|
||||
Row: {
|
||||
avatar_url: string | null
|
||||
email: string | null
|
||||
id: string
|
||||
name: string | null
|
||||
}
|
||||
avatar_url: string | null;
|
||||
email: string | null;
|
||||
id: string;
|
||||
name: string | null;
|
||||
};
|
||||
Insert: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id: string
|
||||
name?: string | null
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
id: string;
|
||||
name?: string | null;
|
||||
};
|
||||
Update: {
|
||||
avatar_url?: string | null
|
||||
email?: string | null
|
||||
id?: string
|
||||
name?: string | null
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
avatar_url?: string | null;
|
||||
email?: string | null;
|
||||
id?: string;
|
||||
name?: string | null;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
tablo_invites: {
|
||||
Row: {
|
||||
id: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: number
|
||||
}
|
||||
id: number;
|
||||
invite_token: string;
|
||||
invited_by: string;
|
||||
invited_email: string;
|
||||
tablo_id: string;
|
||||
};
|
||||
Insert: {
|
||||
id?: number
|
||||
invite_token: string
|
||||
invited_by: string
|
||||
invited_email: string
|
||||
tablo_id: number
|
||||
}
|
||||
id?: number;
|
||||
invite_token: string;
|
||||
invited_by: string;
|
||||
invited_email: string;
|
||||
tablo_id: string;
|
||||
};
|
||||
Update: {
|
||||
id?: number
|
||||
invite_token?: string
|
||||
invited_by?: string
|
||||
invited_email?: string
|
||||
tablo_id?: number
|
||||
}
|
||||
id?: number;
|
||||
invite_token?: string;
|
||||
invited_by?: string;
|
||||
invited_email?: string;
|
||||
tablo_id?: string;
|
||||
};
|
||||
Relationships: [
|
||||
{
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id"
|
||||
columns: ["tablo_id"]
|
||||
isOneToOne: false
|
||||
referencedRelation: "tablos"
|
||||
referencedColumns: ["id"]
|
||||
},
|
||||
]
|
||||
}
|
||||
foreignKeyName: "fk_tablo_invitations_tablo_id";
|
||||
columns: ["tablo_id"];
|
||||
isOneToOne: false;
|
||||
referencedRelation: "tablos";
|
||||
referencedColumns: ["id"];
|
||||
}
|
||||
];
|
||||
};
|
||||
tablos: {
|
||||
Row: {
|
||||
color: string | null
|
||||
created_at: string | null
|
||||
deleted_at: string | null
|
||||
id: number
|
||||
image: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position: number
|
||||
status: string
|
||||
}
|
||||
color: string | null;
|
||||
created_at: string | null;
|
||||
deleted_at: string | null;
|
||||
id: string;
|
||||
image: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
position: number;
|
||||
status: string;
|
||||
};
|
||||
Insert: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: number
|
||||
image?: string | null
|
||||
name: string
|
||||
owner_id: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
color?: string | null;
|
||||
created_at?: string | null;
|
||||
deleted_at?: string | null;
|
||||
id?: string;
|
||||
image?: string | null;
|
||||
name: string;
|
||||
owner_id: string;
|
||||
position?: number;
|
||||
status?: string;
|
||||
};
|
||||
Update: {
|
||||
color?: string | null
|
||||
created_at?: string | null
|
||||
deleted_at?: string | null
|
||||
id?: number
|
||||
image?: string | null
|
||||
name?: string
|
||||
owner_id?: string
|
||||
position?: number
|
||||
status?: string
|
||||
}
|
||||
Relationships: []
|
||||
}
|
||||
}
|
||||
color?: string | null;
|
||||
created_at?: string | null;
|
||||
deleted_at?: string | null;
|
||||
id?: string;
|
||||
image?: string | null;
|
||||
name?: string;
|
||||
owner_id?: string;
|
||||
position?: number;
|
||||
status?: string;
|
||||
};
|
||||
Relationships: [];
|
||||
};
|
||||
};
|
||||
Views: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Functions: {
|
||||
[_ in never]: never
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
}
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired";
|
||||
};
|
||||
CompositeTypes: {
|
||||
[_ in never]: never
|
||||
}
|
||||
}
|
||||
}
|
||||
[_ in never]: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">]
|
||||
type DefaultSchema = Database[Extract<keyof Database, "public">];
|
||||
|
||||
export type Tables<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
|
||||
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||
Row: infer R
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R
|
||||
}
|
||||
? R
|
||||
: never
|
||||
DefaultSchema["Views"])
|
||||
? (DefaultSchema["Tables"] &
|
||||
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
|
||||
Row: infer R;
|
||||
}
|
||||
? R
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesInsert<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Insert: infer I
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I
|
||||
}
|
||||
? I
|
||||
: never
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Insert: infer I;
|
||||
}
|
||||
? I
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type TablesUpdate<
|
||||
DefaultSchemaTableNameOrOptions extends
|
||||
| keyof DefaultSchema["Tables"]
|
||||
| { schema: keyof Database },
|
||||
TableName extends DefaultSchemaTableNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||
Update: infer U
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U
|
||||
}
|
||||
? U
|
||||
: never
|
||||
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
|
||||
Update: infer U;
|
||||
}
|
||||
? U
|
||||
: never
|
||||
: never;
|
||||
|
||||
export type Enums<
|
||||
DefaultSchemaEnumNameOrOptions extends
|
||||
| keyof DefaultSchema["Enums"]
|
||||
| { schema: keyof Database },
|
||||
EnumName extends DefaultSchemaEnumNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
|
||||
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never
|
||||
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
|
||||
: never;
|
||||
|
||||
export type CompositeTypes<
|
||||
PublicCompositeTypeNameOrOptions extends
|
||||
| keyof DefaultSchema["CompositeTypes"]
|
||||
| { schema: keyof Database },
|
||||
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
|
||||
schema: keyof Database
|
||||
schema: keyof Database;
|
||||
}
|
||||
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
|
||||
: never = never,
|
||||
: never = never
|
||||
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
|
||||
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
|
||||
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never
|
||||
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
|
||||
: never;
|
||||
|
||||
export const Constants = {
|
||||
public: {
|
||||
|
|
@ -303,4 +303,4 @@ export const Constants = {
|
|||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
} as const;
|
||||
|
|
|
|||
Loading…
Reference in a new issue