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;
|
fileId?: string | null;
|
||||||
src?: string | null;
|
src?: string | null;
|
||||||
fallback?: ReactElement;
|
fallback?: ReactElement;
|
||||||
|
tenantId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthenticatedImage = ({
|
export const AuthenticatedImage = ({
|
||||||
@ -27,6 +28,7 @@ export const AuthenticatedImage = ({
|
|||||||
fallback,
|
fallback,
|
||||||
className,
|
className,
|
||||||
alt = "Image",
|
alt = "Image",
|
||||||
|
tenantId,
|
||||||
...props
|
...props
|
||||||
}: AuthenticatedImageProps): ReactElement => {
|
}: AuthenticatedImageProps): ReactElement => {
|
||||||
// Helper to extract fileId from backend preview URL
|
// Helper to extract fileId from backend preview URL
|
||||||
@ -109,11 +111,13 @@ export const AuthenticatedImage = ({
|
|||||||
const fetchPromise = (async () => {
|
const fetchPromise = (async () => {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (extractedFileId) {
|
if (extractedFileId) {
|
||||||
url = await fileService.getPreview(extractedFileId);
|
url = await fileService.getPreview(extractedFileId, tenantId || undefined);
|
||||||
} else {
|
} else {
|
||||||
// If useBackendUrl is true, src is guaranteed to be non-null
|
// 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!, {
|
const response = await apiClient.get(src!, {
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
url = URL.createObjectURL(response.data);
|
url = URL.createObjectURL(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -457,6 +457,7 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
accent_color,
|
accent_color,
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
|
// 1. Create the tenant first (without logo/favicon metadata)
|
||||||
const tenantData = {
|
const tenantData = {
|
||||||
...restTenantDetails,
|
...restTenantDetails,
|
||||||
module_ids: selectedModules.length > 0 ? selectedModules : undefined,
|
module_ids: selectedModules.length > 0 ? selectedModules : undefined,
|
||||||
@ -468,16 +469,86 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
primary_color: primary_color || undefined,
|
primary_color: primary_color || undefined,
|
||||||
secondary_color: secondary_color || undefined,
|
secondary_color: secondary_color || undefined,
|
||||||
accent_color: accent_color || undefined,
|
accent_color: accent_color || undefined,
|
||||||
logo_file_path: logoFileUrl || undefined,
|
logo_file_path: undefined,
|
||||||
logo_file_attachment_uuid: logoFileAttachmentUuid || undefined,
|
logo_file_attachment_uuid: undefined,
|
||||||
favicon_file_path: faviconFileUrl || undefined,
|
favicon_file_path: undefined,
|
||||||
favicon_file_attachment_uuid:
|
favicon_file_attachment_uuid: undefined,
|
||||||
faviconFileAttachmentUuid || undefined,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await tenantService.create(tenantData);
|
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";
|
const message = response.message || "Tenant created successfully";
|
||||||
showToast.success(message);
|
showToast.success(message);
|
||||||
navigate("/tenants");
|
navigate("/tenants");
|
||||||
@ -1115,38 +1186,7 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
const previewUrl = URL.createObjectURL(file);
|
const previewUrl = URL.createObjectURL(file);
|
||||||
setLogoFile(file);
|
setLogoFile(file);
|
||||||
setLogoPreviewUrl(previewUrl);
|
setLogoPreviewUrl(previewUrl);
|
||||||
setIsUploadingLogo(true);
|
setLogoError(null);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
@ -1243,40 +1283,7 @@ const CreateTenantWizard = (): ReactElement => {
|
|||||||
const previewUrl = URL.createObjectURL(file);
|
const previewUrl = URL.createObjectURL(file);
|
||||||
setFaviconFile(file);
|
setFaviconFile(file);
|
||||||
setFaviconPreviewUrl(previewUrl);
|
setFaviconPreviewUrl(previewUrl);
|
||||||
setIsUploadingFavicon(true);
|
setFaviconError(null);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
|
|||||||
@ -1333,6 +1333,7 @@ const EditTenant = (): ReactElement => {
|
|||||||
key={logoPreviewUrl}
|
key={logoPreviewUrl}
|
||||||
fileId={logoFileAttachmentUuid}
|
fileId={logoFileAttachmentUuid}
|
||||||
src={logoPreviewUrl}
|
src={logoPreviewUrl}
|
||||||
|
tenantId={id}
|
||||||
alt="Logo preview"
|
alt="Logo preview"
|
||||||
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="max-w-full h-20 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{ display: "block", maxHeight: "80px" }}
|
style={{ display: "block", maxHeight: "80px" }}
|
||||||
@ -1462,6 +1463,7 @@ const EditTenant = (): ReactElement => {
|
|||||||
key={faviconPreviewUrl || faviconFileUrl}
|
key={faviconPreviewUrl || faviconFileUrl}
|
||||||
fileId={faviconFileAttachmentUuid}
|
fileId={faviconFileAttachmentUuid}
|
||||||
src={faviconPreviewUrl || faviconFileUrl}
|
src={faviconPreviewUrl || faviconFileUrl}
|
||||||
|
tenantId={id}
|
||||||
alt="Favicon preview"
|
alt="Favicon preview"
|
||||||
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
className="w-16 h-16 object-contain border border-[#d1d5db] rounded-md p-2 bg-white"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -291,8 +291,12 @@ export const fileAttachmentService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** GET /files/:id/download (blob) */
|
/** GET /files/:id/download (blob) */
|
||||||
download: async (id: string, filename?: string): Promise<void> => {
|
download: async (id: string, filename?: string, tenantId?: string): Promise<void> => {
|
||||||
const response = await apiClient.get(`/files/${id}/download`, { responseType: 'blob' });
|
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 url = URL.createObjectURL(response.data);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = url;
|
a.href = url;
|
||||||
@ -302,8 +306,12 @@ export const fileAttachmentService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/** GET /files/:id/preview — returns blob URL for inline preview */
|
/** GET /files/:id/preview — returns blob URL for inline preview */
|
||||||
getPreviewUrl: async (id: string): Promise<string> => {
|
getPreviewUrl: async (id: string, tenantId?: string): Promise<string> => {
|
||||||
const response = await apiClient.get(`/files/${id}/preview`, { responseType: 'blob' });
|
const headers = tenantId ? { 'x-tenant-id': tenantId } : undefined;
|
||||||
|
const response = await apiClient.get(`/files/${id}/preview`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers,
|
||||||
|
});
|
||||||
return URL.createObjectURL(response.data);
|
return URL.createObjectURL(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -60,13 +60,19 @@ export const fileService = {
|
|||||||
if (entityId) formData.append('entity_id', entityId);
|
if (entityId) formData.append('entity_id', entityId);
|
||||||
if (tenantId) formData.append('tenantId', tenantId);
|
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;
|
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`, {
|
const response = await apiClient.get(`/files/${fileId}/preview`, {
|
||||||
responseType: 'blob',
|
responseType: 'blob',
|
||||||
|
headers,
|
||||||
});
|
});
|
||||||
return URL.createObjectURL(response.data);
|
return URL.createObjectURL(response.data);
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user