feat: add org ID cookie management and logo upload/remove hooks

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-04-02 22:03:59 +02:00
parent acc83401f4
commit 2e9ab46be8
No known key found for this signature in database
2 changed files with 93 additions and 1 deletions

View file

@ -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: () => {

View file

@ -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<OrganizationResponse>("/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<string>((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",
});
},
});
};