feat: add tenant-scoped file operations and update tenant creation flow to support multi-step branding uploads

This commit is contained in:
Yashwin 2026-06-03 18:41:24 +05:30
parent 7a598d01ae
commit 28f3d886ba
5 changed files with 105 additions and 78 deletions

View File

@ -19,6 +19,7 @@ interface AuthenticatedImageProps extends Omit<
fileId?: string | null;
src?: string | null;
fallback?: ReactElement;
tenantId?: string | null;
}
export const AuthenticatedImage = ({
@ -27,6 +28,7 @@ export const AuthenticatedImage = ({
fallback,
className,
alt = "Image",
tenantId,
...props
}: AuthenticatedImageProps): ReactElement => {
// Helper to extract fileId from backend preview URL
@ -109,11 +111,13 @@ export const AuthenticatedImage = ({
const fetchPromise = (async () => {
let url: string;
if (extractedFileId) {
url = await fileService.getPreview(extractedFileId);
url = await fileService.getPreview(extractedFileId, tenantId || undefined);
} else {
// If useBackendUrl is true, src is guaranteed to be non-null
const headers = tenantId ? { "x-tenant-id": tenantId } : undefined;
const response = await apiClient.get(src!, {
responseType: "blob",
headers,
});
url = URL.createObjectURL(response.data);
}

View File

@ -457,6 +457,7 @@ const CreateTenantWizard = (): ReactElement => {
accent_color,
} = settings;
// 1. Create the tenant first (without logo/favicon metadata)
const tenantData = {
...restTenantDetails,
module_ids: selectedModules.length > 0 ? selectedModules : undefined,
@ -468,16 +469,86 @@ const CreateTenantWizard = (): ReactElement => {
primary_color: primary_color || undefined,
secondary_color: secondary_color || undefined,
accent_color: accent_color || undefined,
logo_file_path: logoFileUrl || undefined,
logo_file_attachment_uuid: logoFileAttachmentUuid || undefined,
favicon_file_path: faviconFileUrl || undefined,
favicon_file_attachment_uuid:
faviconFileAttachmentUuid || undefined,
logo_file_path: undefined,
logo_file_attachment_uuid: undefined,
favicon_file_path: undefined,
favicon_file_attachment_uuid: undefined,
},
},
};
const response = await tenantService.create(tenantData);
const createdTenant = response.data;
const createdTenantId = createdTenant.id;
const slug = createdTenant.slug || tenantDetails.slug;
// 2. Upload logo and favicon under the created tenant context
let finalLogoUuid = logoFileAttachmentUuid;
let finalLogoPath = logoFileUrl;
let finalFaviconUuid = faviconFileAttachmentUuid;
let finalFaviconPath = faviconFileUrl;
if (logoFile) {
setIsUploadingLogo(true);
try {
const uploadRes = await fileService.upload(
logoFile,
slug || "tenant",
generateUUID(),
createdTenantId
);
finalLogoUuid = uploadRes.data.id;
finalLogoPath = `${getBaseUrlWithProtocol()}/files/${finalLogoUuid}/preview`;
} catch (uploadErr) {
console.error("Failed to upload logo:", uploadErr);
showToast.error("Tenant created, but failed to upload logo.");
} finally {
setIsUploadingLogo(false);
}
}
if (faviconFile) {
setIsUploadingFavicon(true);
try {
const uploadRes = await fileService.upload(
faviconFile,
slug || "tenant",
generateUUID(),
createdTenantId
);
finalFaviconUuid = uploadRes.data.id;
finalFaviconPath = `${getBaseUrlWithProtocol()}/files/${finalFaviconUuid}/preview`;
} catch (uploadErr) {
console.error("Failed to upload favicon:", uploadErr);
showToast.error("Tenant created, but failed to upload favicon.");
} finally {
setIsUploadingFavicon(false);
}
}
// 3. Update the tenant record with the newly uploaded branding assets
if (finalLogoUuid || finalFaviconUuid) {
const updateData = {
name: tenantDetails.name,
slug: slug || tenantDetails.slug,
status: tenantDetails.status,
settings: {
enable_sso,
enable_2fa,
branding: {
primary_color: primary_color || undefined,
secondary_color: secondary_color || undefined,
accent_color: accent_color || undefined,
logo_file_path: finalLogoPath || undefined,
logo_file_attachment_uuid: finalLogoUuid || undefined,
favicon_file_path: finalFaviconPath || undefined,
favicon_file_attachment_uuid: finalFaviconUuid || undefined,
},
},
};
await tenantService.update(createdTenantId, updateData);
}
const message = response.message || "Tenant created successfully";
showToast.success(message);
navigate("/tenants");
@ -1115,38 +1186,7 @@ const CreateTenantWizard = (): ReactElement => {
const previewUrl = URL.createObjectURL(file);
setLogoFile(file);
setLogoPreviewUrl(previewUrl);
setIsUploadingLogo(true);
try {
const response = await fileService.upload(
file,
"tenant",
generateUUID(),
);
const fileId = response.data.id;
setLogoFileAttachmentUuid(fileId);
const baseUrl = getBaseUrlWithProtocol();
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
setLogoFileUrl(formattedUrl);
setLogoError(null);
showToast.success("Logo uploaded successfully");
} catch (err: any) {
const errorMessage =
err?.response?.data?.error?.message ||
err?.response?.data?.message ||
err?.message ||
"Failed to upload logo. Please try again.";
showToast.error(errorMessage);
setLogoFile(null);
URL.revokeObjectURL(previewUrl);
setLogoPreviewUrl(null);
setLogoFileUrl(null);
setLogoFileAttachmentUuid(null);
} finally {
setIsUploadingLogo(false);
}
setLogoError(null);
}
}}
className="hidden"
@ -1243,40 +1283,7 @@ const CreateTenantWizard = (): ReactElement => {
const previewUrl = URL.createObjectURL(file);
setFaviconFile(file);
setFaviconPreviewUrl(previewUrl);
setIsUploadingFavicon(true);
try {
const response = await fileService.upload(
file,
"tenant",
generateUUID(),
);
const fileId = response.data.id;
setFaviconFileAttachmentUuid(fileId);
const baseUrl = getBaseUrlWithProtocol();
const formattedUrl = `${baseUrl}/files/${fileId}/preview`;
setFaviconFileUrl(formattedUrl);
setFaviconError(null);
showToast.success(
"Favicon uploaded successfully",
);
} catch (err: any) {
const errorMessage =
err?.response?.data?.error?.message ||
err?.response?.data?.message ||
err?.message ||
"Failed to upload favicon. Please try again.";
showToast.error(errorMessage);
setFaviconFile(null);
URL.revokeObjectURL(previewUrl);
setFaviconPreviewUrl(null);
setFaviconFileUrl(null);
setFaviconFileAttachmentUuid(null);
} finally {
setIsUploadingFavicon(false);
}
setFaviconError(null);
}
}}
className="hidden"

View File

@ -1333,6 +1333,7 @@ const EditTenant = (): ReactElement => {
key={logoPreviewUrl}
fileId={logoFileAttachmentUuid}
src={logoPreviewUrl}
tenantId={id}
alt="Logo preview"
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
style={{ display: "block", maxHeight: "80px" }}
@ -1462,6 +1463,7 @@ const EditTenant = (): ReactElement => {
key={faviconPreviewUrl || faviconFileUrl}
fileId={faviconFileAttachmentUuid}
src={faviconPreviewUrl || faviconFileUrl}
tenantId={id}
alt="Favicon preview"
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
style={{

View File

@ -291,8 +291,12 @@ export const fileAttachmentService = {
},
/** GET /files/:id/download (blob) */
download: async (id: string, filename?: string): Promise<void> => {
const response = await apiClient.get(`/files/${id}/download`, { responseType: 'blob' });
download: async (id: string, filename?: string, tenantId?: string): Promise<void> => {
const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined;
const response = await apiClient.get(`/files/${id}/download`, {
responseType: 'blob',
headers,
});
const url = URL.createObjectURL(response.data);
const a = document.createElement('a');
a.href = url;
@ -302,8 +306,12 @@ export const fileAttachmentService = {
},
/** GET /files/:id/preview — returns blob URL for inline preview */
getPreviewUrl: async (id: string): Promise<string> => {
const response = await apiClient.get(`/files/${id}/preview`, { responseType: 'blob' });
getPreviewUrl: async (id: string, tenantId?: string): Promise<string> => {
const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined;
const response = await apiClient.get(`/files/${id}/preview`, {
responseType: 'blob',
headers,
});
return URL.createObjectURL(response.data);
},

View File

@ -60,13 +60,19 @@ export const fileService = {
if (entityId) formData.append('entity_id', entityId);
if (tenantId) formData.append('tenantId', tenantId);
const response = await apiClient.post<FileUploadResponse>('/files/upload', formData);
const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined;
const response = await apiClient.post<FileUploadResponse>('/files/upload', formData, {
headers
});
return response.data;
},
getPreview: async (fileId: string): Promise<string> => {
getPreview: async (fileId: string, tenantId?: string): Promise<string> => {
const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined;
const response = await apiClient.get(`/files/${fileId}/preview`, {
responseType: 'blob',
headers,
});
return URL.createObjectURL(response.data);
},