From 2e9ab46be87ed029344192374bb3d391dce58cb4 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Thu, 2 Apr 2026 22:03:59 +0200 Subject: [PATCH] feat: add org ID cookie management and logo upload/remove hooks Co-Authored-By: Claude Sonnet 4.6 (1M context) --- apps/main/src/hooks/auth.ts | 2 + apps/main/src/hooks/organization.ts | 92 ++++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/apps/main/src/hooks/auth.ts b/apps/main/src/hooks/auth.ts index 604d040..c0fc474 100644 --- a/apps/main/src/hooks/auth.ts +++ b/apps/main/src/hooks/auth.ts @@ -7,6 +7,7 @@ import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { match } from "ts-pattern"; import { api } from "../lib/api"; +import { clearOrgIdCookie } from "./organization"; import { DEFAULT_SIGNUP_BILLING_INTENT, PENDING_BILLING_CHECKOUT_PLAN_KEY, @@ -265,6 +266,7 @@ export function useLogout() { mutationFn: async () => { const { error } = await supabase.auth.signOut(); if (error) throw error; + clearOrgIdCookie(); queryClient.removeQueries(); }, onSuccess: () => { diff --git a/apps/main/src/hooks/organization.ts b/apps/main/src/hooks/organization.ts index 0ce525a..c2706e5 100644 --- a/apps/main/src/hooks/organization.ts +++ b/apps/main/src/hooks/organization.ts @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "@xtablo/shared"; +import { useEffect } from "react"; import { useAuthedApi } from "./auth"; export interface OrganizationSummary { @@ -8,6 +9,7 @@ export interface OrganizationSummary { plan: string; member_count: number; tablo_count: number; + logo_url: string | null; } export interface OrganizationMember { @@ -51,16 +53,35 @@ export interface OrganizationInvite { } | null; } +function setOrgIdCookie(orgId: number): void { + document.cookie = `x-org-id=${orgId}; path=/; secure; samesite=lax; max-age=31536000`; +} + +function clearOrgIdCookie(): void { + document.cookie = "x-org-id=; path=/; secure; samesite=lax; max-age=0"; +} + +export { clearOrgIdCookie }; + export const useOrganization = () => { const api = useAuthedApi(); - return useQuery({ + const query = useQuery({ queryKey: ["organization"], queryFn: async () => { const { data } = await api.get("/api/v1/users/organization"); return data; }, }); + + // Set org ID cookie for dynamic manifest + useEffect(() => { + if (query.data?.organization?.id) { + setOrgIdCookie(query.data.organization.id); + } + }, [query.data?.organization?.id]); + + return query; }; export const useUpdateOrganization = () => { @@ -143,3 +164,72 @@ export const useRemoveOrganizationMember = () => { }, }); }; + +export const useUploadOrgLogo = () => { + const api = useAuthedApi(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (file: File) => { + const base64Content = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result === "string") { + resolve(reader.result.split(",")[1]); + } else { + reject(new Error("Failed to read file")); + } + }; + reader.onerror = () => reject(new Error("Error reading file")); + reader.readAsDataURL(file); + }); + + const { data } = await api.patch("/api/v1/users/organization", { + logo: { content: base64Content, contentType: file.type }, + }); + return data; + }, + onSuccess: () => { + toast.add({ + title: "Logo mis à jour", + description: "Le logo de l'organisation a bien été enregistré", + type: "success", + }); + queryClient.invalidateQueries({ queryKey: ["organization"] }); + }, + onError: (error: Error) => { + toast.add({ + title: "Erreur", + description: error.message || "Impossible de mettre à jour le logo", + type: "error", + }); + }, + }); +}; + +export const useRemoveOrgLogo = () => { + const api = useAuthedApi(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { + const { data } = await api.patch("/api/v1/users/organization", { logo: null }); + return data; + }, + onSuccess: () => { + toast.add({ + title: "Logo supprimé", + description: "Le logo de l'organisation a été supprimé", + type: "success", + }); + queryClient.invalidateQueries({ queryKey: ["organization"] }); + }, + onError: (error: Error) => { + toast.add({ + title: "Erreur", + description: error.message || "Impossible de supprimer le logo", + type: "error", + }); + }, + }); +};