From 28f3d886ba36a0b0ccc4abd0cf870fbd3dc242e5 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Wed, 3 Jun 2026 18:41:24 +0530 Subject: [PATCH] feat: add tenant-scoped file operations and update tenant creation flow to support multi-step branding uploads --- src/components/shared/AuthenticatedImage.tsx | 6 +- src/pages/superadmin/CreateTenantWizard.tsx | 149 ++++++++++--------- src/pages/superadmin/EditTenant.tsx | 2 + src/services/file-attachment-service.ts | 16 +- src/services/file-service.ts | 10 +- 5 files changed, 105 insertions(+), 78 deletions(-) diff --git a/src/components/shared/AuthenticatedImage.tsx b/src/components/shared/AuthenticatedImage.tsx index e06028c..de01698 100644 --- a/src/components/shared/AuthenticatedImage.tsx +++ b/src/components/shared/AuthenticatedImage.tsx @@ -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); } diff --git a/src/pages/superadmin/CreateTenantWizard.tsx b/src/pages/superadmin/CreateTenantWizard.tsx index 69800b7..103bd25 100644 --- a/src/pages/superadmin/CreateTenantWizard.tsx +++ b/src/pages/superadmin/CreateTenantWizard.tsx @@ -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" diff --git a/src/pages/superadmin/EditTenant.tsx b/src/pages/superadmin/EditTenant.tsx index 74b1d6e..05fa045 100644 --- a/src/pages/superadmin/EditTenant.tsx +++ b/src/pages/superadmin/EditTenant.tsx @@ -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={{ diff --git a/src/services/file-attachment-service.ts b/src/services/file-attachment-service.ts index 56c7f08..d64169a 100644 --- a/src/services/file-attachment-service.ts +++ b/src/services/file-attachment-service.ts @@ -291,8 +291,12 @@ export const fileAttachmentService = { }, /** GET /files/:id/download (blob) */ - download: async (id: string, filename?: string): Promise => { - const response = await apiClient.get(`/files/${id}/download`, { responseType: 'blob' }); + download: async (id: string, filename?: string, tenantId?: string): Promise => { + 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 => { - const response = await apiClient.get(`/files/${id}/preview`, { responseType: 'blob' }); + getPreviewUrl: async (id: string, tenantId?: string): Promise => { + const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined; + const response = await apiClient.get(`/files/${id}/preview`, { + responseType: 'blob', + headers, + }); return URL.createObjectURL(response.data); }, diff --git a/src/services/file-service.ts b/src/services/file-service.ts index bc33e2c..501fc5c 100644 --- a/src/services/file-service.ts +++ b/src/services/file-service.ts @@ -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('/files/upload', formData); + const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined; + + const response = await apiClient.post('/files/upload', formData, { + headers + }); return response.data; }, - getPreview: async (fileId: string): Promise => { + getPreview: async (fileId: string, tenantId?: string): Promise => { + const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined; const response = await apiClient.get(`/files/${fileId}/preview`, { responseType: 'blob', + headers, }); return URL.createObjectURL(response.data); },