feat: add tenant-scoped file operations and update tenant creation flow to support multi-step branding uploads
This commit is contained in:
parent
7a598d01ae
commit
28f3d886ba
@ -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);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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={{
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user