diff --git a/frontend/src/app-components/attachment/AttachmentInput.tsx b/frontend/src/app-components/attachment/AttachmentInput.tsx index 28c7a5ec..67edd5da 100644 --- a/frontend/src/app-components/attachment/AttachmentInput.tsx +++ b/frontend/src/app-components/attachment/AttachmentInput.tsx @@ -1,18 +1,19 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { Box, FormHelperText, FormLabel } from "@mui/material"; import { forwardRef } from "react"; import { useGet } from "@/hooks/crud/useGet"; import { useHasPermission } from "@/hooks/useHasPermission"; import { EntityType } from "@/services/types"; -import { IAttachment } from "@/types/attachment.types"; +import { IAttachment, TAttachmentContext } from "@/types/attachment.types"; import { PermissionAction } from "@/types/permission.types"; import AttachmentThumbnail from "./AttachmentThumbnail"; @@ -28,6 +29,7 @@ type AttachmentThumbnailProps = { onChange?: (id: string | null, mimeType: string | null) => void; error?: boolean; helperText?: string; + context: TAttachmentContext; }; const AttachmentInput = forwardRef( @@ -42,6 +44,7 @@ const AttachmentInput = forwardRef( onChange, error, helperText, + context, }, ref, ) => { @@ -81,6 +84,7 @@ const AttachmentInput = forwardRef( accept={accept} enableMediaLibrary={enableMediaLibrary} onChange={handleChange} + context={context} /> ) : null} {helperText ? ( diff --git a/frontend/src/app-components/attachment/AttachmentUploader.tsx b/frontend/src/app-components/attachment/AttachmentUploader.tsx index 73cdf176..a2d662c1 100644 --- a/frontend/src/app-components/attachment/AttachmentUploader.tsx +++ b/frontend/src/app-components/attachment/AttachmentUploader.tsx @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import CloudUploadIcon from "@mui/icons-material/CloudUpload"; import FolderCopyIcon from "@mui/icons-material/FolderCopy"; import { Box, Button, Divider, Grid, styled, Typography } from "@mui/material"; @@ -16,7 +17,7 @@ import { getDisplayDialogs, useDialog } from "@/hooks/useDialog"; import { useToast } from "@/hooks/useToast"; import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; -import { IAttachment } from "@/types/attachment.types"; +import { IAttachment, TAttachmentContext } from "@/types/attachment.types"; import { AttachmentDialog } from "./AttachmentDialog"; import AttachmentThumbnail from "./AttachmentThumbnail"; @@ -67,6 +68,7 @@ export type FileUploadProps = { enableMediaLibrary?: boolean; onChange?: (data?: IAttachment | null) => void; onUploadComplete?: () => void; + context: TAttachmentContext; }; const AttachmentUploader: FC = ({ @@ -74,6 +76,7 @@ const AttachmentUploader: FC = ({ enableMediaLibrary, onChange, onUploadComplete, + context, }) => { const [attachment, setAttachment] = useState( undefined, @@ -97,34 +100,40 @@ const AttachmentUploader: FC = ({ e.stopPropagation(); e.preventDefault(); }; + const handleUpload = (file: File | null) => { + if (file) { + const acceptedTypes = accept.split(","); + const isValidType = acceptedTypes.some((mimeType) => { + const [type, subtype] = mimeType.split("/"); + + if (!type || !subtype) return false; // Ensure valid MIME type + + return ( + file.type === mimeType || + (subtype === "*" && file.type.startsWith(`${type}/`)) + ); + }); + + if (!isValidType) { + toast.error(t("message.invalid_file_type")); + + return; + } + uploadAttachment({ file, context }); + } + }; const handleChange = (event: ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { const file = event.target.files.item(0); - if (file) { - const acceptedTypes = accept.split(","); - const isValidType = acceptedTypes.some( - (type) => - file.type === type || file.name.endsWith(type.replace(".*", "")), - ); - - if (!isValidType) { - toast.error(t("message.invalid_file_type")); - - return; - } - - uploadAttachment(file); - } + handleUpload(file); } }; const onDrop = (event: DragEvent) => { if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { const file = event.dataTransfer.files.item(0); - if (file) { - uploadAttachment(file); - } + handleUpload(file); } }; diff --git a/frontend/src/app-components/attachment/MultipleAttachmentInput.tsx b/frontend/src/app-components/attachment/MultipleAttachmentInput.tsx index 6ba968cc..80b76283 100644 --- a/frontend/src/app-components/attachment/MultipleAttachmentInput.tsx +++ b/frontend/src/app-components/attachment/MultipleAttachmentInput.tsx @@ -1,17 +1,18 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { Box, Button, FormHelperText, FormLabel } from "@mui/material"; import { forwardRef, useState } from "react"; import { useHasPermission } from "@/hooks/useHasPermission"; import { EntityType } from "@/services/types"; -import { IAttachment } from "@/types/attachment.types"; +import { IAttachment, TAttachmentContext } from "@/types/attachment.types"; import { PermissionAction } from "@/types/permission.types"; import AttachmentThumbnail from "./AttachmentThumbnail"; @@ -27,6 +28,7 @@ type MultipleAttachmentInputProps = { onChange?: (ids: string[]) => void; error?: boolean; helperText?: string; + context: TAttachmentContext; }; const MultipleAttachmentInput = forwardRef< @@ -44,6 +46,7 @@ const MultipleAttachmentInput = forwardRef< onChange, error, helperText, + context, }, ref, ) => { @@ -106,6 +109,7 @@ const MultipleAttachmentInput = forwardRef< accept={accept} enableMediaLibrary={enableMediaLibrary} onChange={(attachment) => handleChange(attachment)} + context={context} /> )} {helperText && ( diff --git a/frontend/src/components/contents/ContentDialog.tsx b/frontend/src/components/contents/ContentDialog.tsx index c04b3a75..885dfe73 100644 --- a/frontend/src/components/contents/ContentDialog.tsx +++ b/frontend/src/components/contents/ContentDialog.tsx @@ -116,6 +116,7 @@ const ContentFieldInput: React.FC = ({ value={field.value?.payload?.id} accept={MIME_TYPES["images"].join(",")} format="full" + context="content_attachment" /> ); default: diff --git a/frontend/src/components/contents/ContentImportDialog.tsx b/frontend/src/components/contents/ContentImportDialog.tsx index 4ed2a4f8..61855149 100644 --- a/frontend/src/components/contents/ContentImportDialog.tsx +++ b/frontend/src/components/contents/ContentImportDialog.tsx @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import CloseIcon from "@mui/icons-material/Close"; import { Button, Dialog, DialogActions, DialogContent } from "@mui/material"; import { FC, useState } from "react"; @@ -80,6 +81,7 @@ export const ContentImportDialog: FC = ({ }} label="" value={attachmentId} + context="content_attachment" /> diff --git a/frontend/src/components/settings/SettingInput.tsx b/frontend/src/components/settings/SettingInput.tsx index f09ef9be..fd41381c 100644 --- a/frontend/src/components/settings/SettingInput.tsx +++ b/frontend/src/components/settings/SettingInput.tsx @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import KeyIcon from "@mui/icons-material/Key"; import { FormControlLabel, MenuItem, Switch } from "@mui/material"; import { ControllerRenderProps } from "react-hook-form"; @@ -185,6 +186,7 @@ const SettingInput: React.FC = ({ accept={MIME_TYPES["images"].join(",")} format="full" size={128} + context="setting_attachment" /> ); @@ -197,6 +199,7 @@ const SettingInput: React.FC = ({ accept={MIME_TYPES["images"].join(",")} format="full" size={128} + context="setting_attachment" /> ); default: diff --git a/frontend/src/components/visual-editor/form/AttachmentMessageForm.tsx b/frontend/src/components/visual-editor/form/AttachmentMessageForm.tsx index 35da99fb..0579d9dd 100644 --- a/frontend/src/components/visual-editor/form/AttachmentMessageForm.tsx +++ b/frontend/src/components/visual-editor/form/AttachmentMessageForm.tsx @@ -69,6 +69,7 @@ const AttachmentMessageForm = () => { }, }); }} + context="block_attachment" /> ); }} diff --git a/frontend/src/hooks/crud/useUpload.tsx b/frontend/src/hooks/crud/useUpload.tsx index 51202889..342645a9 100644 --- a/frontend/src/hooks/crud/useUpload.tsx +++ b/frontend/src/hooks/crud/useUpload.tsx @@ -9,6 +9,7 @@ import { useMutation, useQueryClient } from "react-query"; import { QueryType, TMutationOptions } from "@/services/types"; +import { TAttachmentContext } from "@/types/attachment.types"; import { IBaseSchema, IDynamicProps, TType } from "@/types/base.types"; import { useEntityApiClient } from "../useApiClient"; @@ -23,7 +24,12 @@ export const useUpload = < >( entity: TEntity, options?: Omit< - TMutationOptions, + TMutationOptions< + TBasic, + Error, + { file: File; context: TAttachmentContext }, + TBasic + >, "mutationFn" | "mutationKey" >, ) => { @@ -33,8 +39,8 @@ export const useUpload = < const { invalidate = true, ...otherOptions } = options || {}; return useMutation({ - mutationFn: async (variables: File) => { - const data = await api.upload(variables); + mutationFn: async ({ file, context }) => { + const data = await api.upload(file, context); const { entities, result } = normalizeAndCache(data); // Invalidate all counts & collections diff --git a/frontend/src/services/api.class.ts b/frontend/src/services/api.class.ts index 091c691d..dc062504 100644 --- a/frontend/src/services/api.class.ts +++ b/frontend/src/services/api.class.ts @@ -9,6 +9,7 @@ import { AxiosInstance, AxiosResponse } from "axios"; +import { TAttachmentContext } from "@/types/attachment.types"; import { ILoginAttributes } from "@/types/auth/login.types"; import { IUserPermissions } from "@/types/auth/permission.types"; import { StatsType } from "@/types/bot-stat.types"; @@ -301,7 +302,7 @@ export class EntityApiClient extends ApiClient { return data; } - async upload(file: File) { + async upload(file: File, context?: TAttachmentContext) { const { _csrf } = await this.getCsrf(); const formData = new FormData(); @@ -311,11 +312,17 @@ export class EntityApiClient extends ApiClient { TBasic[], AxiosResponse, FormData - >(`${ROUTES[this.type]}/upload?_csrf=${_csrf}`, formData, { - headers: { - "Content-Type": "multipart/form-data", + >( + `${ROUTES[this.type]}/upload?_csrf=${_csrf}${ + context ? `&context=${context}` : "" + }`, + formData, + { + headers: { + "Content-Type": "multipart/form-data", + }, }, - }); + ); return data[0]; } diff --git a/frontend/src/types/attachment.types.ts b/frontend/src/types/attachment.types.ts index 24570254..adfdc8aa 100644 --- a/frontend/src/types/attachment.types.ts +++ b/frontend/src/types/attachment.types.ts @@ -1,14 +1,43 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ -import { Format } from "@/services/types"; -import { IBaseSchema, IFormat } from "./base.types"; +import { EntityType, Format } from "@/services/types"; + +import { IBaseSchema, IFormat, OmitPopulate } from "./base.types"; +import { ISubscriber } from "./subscriber.types"; +import { IUser } from "./user.types"; + +/** + * Defines the types of owners for an attachment, + * indicating whether the file belongs to a User or a Subscriber. + */ +export enum AttachmentOwnerType { + User = "User", + Subscriber = "Subscriber", +} + +export type TAttachmentOwnerType = `${AttachmentOwnerType}`; + +/** + * Defines the various contexts in which an attachment can exist. + * These contexts influence how the attachment is uploaded, stored, and accessed: + */ +export enum AttachmentContext { + SettingAttachment = "setting_attachment", // Attachments related to app settings, restricted to users with specific permissions. + UserAvatar = "user_avatar", // Avatar files for users, only the current user can upload, accessible to those with appropriate permissions. + SubscriberAvatar = "subscriber_avatar", // Avatar files for subscribers, uploaded programmatically, accessible to authorized users. + BlockAttachment = "block_attachment", // Files sent by the bot, public or private based on the channel and user authentication. + ContentAttachment = "content_attachment", // Files in the knowledge base, usually public but could vary based on specific needs. + MessageAttachment = "message_attachment", // Files sent or received via messages, uploaded programmatically, accessible to users with inbox permissions.; +} + +export type TAttachmentContext = `${AttachmentContext}`; export interface IAttachmentAttributes { name: string; @@ -17,8 +46,21 @@ export interface IAttachmentAttributes { location: string; url: string; channel?: Record; + context: TAttachmentContext; + ownerType: TAttachmentOwnerType; + owner: string | null; } -export interface IAttachmentStub extends IBaseSchema, IAttachmentAttributes {} +export interface IAttachmentStub + extends IBaseSchema, + OmitPopulate {} -export interface IAttachment extends IAttachmentStub, IFormat {} +export interface IAttachment extends IAttachmentStub, IFormat { + owner: string | null; +} + +export interface ISubscriberAttachmentFull + extends IAttachmentStub, + IFormat { + owner: (ISubscriber | IUser)[]; +} diff --git a/frontend/src/types/base.types.ts b/frontend/src/types/base.types.ts index d20d123a..77dd4b11 100644 --- a/frontend/src/types/base.types.ts +++ b/frontend/src/types/base.types.ts @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { GridPaginationModel, GridSortModel } from "@mui/x-data-grid"; import { EntityType, Format } from "@/services/types"; @@ -109,7 +110,7 @@ export const POPULATE_BY_TYPE = { [EntityType.MENUTREE]: [], [EntityType.LANGUAGE]: [], [EntityType.TRANSLATION]: [], - [EntityType.ATTACHMENT]: [], + [EntityType.ATTACHMENT]: ["owner"], [EntityType.CUSTOM_BLOCK]: [], [EntityType.CUSTOM_BLOCK_SETTINGS]: [], [EntityType.CHANNEL]: [],