diff --git a/frontend/src/models/workspace.js b/frontend/src/models/workspace.js index d0038874..ddf5e9f6 100644 --- a/frontend/src/models/workspace.js +++ b/frontend/src/models/workspace.js @@ -238,6 +238,53 @@ const Workspace = { }); }, threads: WorkspaceThread, + + uploadPfp: async function (formData, slug) { + return await fetch(`${API_BASE}/workspace/${slug}/upload-pfp`, { + method: "POST", + body: formData, + headers: baseHeaders(), + }) + .then((res) => { + if (!res.ok) throw new Error("Error uploading pfp."); + return { success: true, error: null }; + }) + .catch((e) => { + console.log(e); + return { success: false, error: e.message }; + }); + }, + + fetchPfp: async function (slug) { + return await fetch(`${API_BASE}/workspace/${slug}/pfp`, { + method: "GET", + cache: "no-cache", + headers: baseHeaders(), + }) + .then((res) => { + if (res.ok && res.status !== 204) return res.blob(); + throw new Error("Failed to fetch pfp."); + }) + .then((blob) => (blob ? URL.createObjectURL(blob) : null)) + .catch((e) => { + console.log(e); + return null; + }); + }, + removePfp: async function (slug) { + return await fetch(`${API_BASE}/workspace/${slug}/remove-pfp`, { + method: "DELETE", + headers: baseHeaders(), + }) + .then((res) => { + if (res.ok) return { success: true, error: null }; + throw new Error("Failed to remove pfp."); + }) + .catch((e) => { + console.log(e); + return { success: false, error: e.message }; + }); + }, }; export default Workspace; diff --git a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx index ee00143e..d7d272c2 100644 --- a/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx +++ b/frontend/src/pages/WorkspaceSettings/GeneralAppearance/index.jsx @@ -6,23 +6,57 @@ import VectorCount from "./VectorCount"; import WorkspaceName from "./WorkspaceName"; import SuggestedChatMessages from "./SuggestedChatMessages"; import DeleteWorkspace from "./DeleteWorkspace"; +import { Plus } from "@phosphor-icons/react"; export default function GeneralInfo({ slug }) { const [workspace, setWorkspace] = useState(null); const [hasChanges, setHasChanges] = useState(false); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); + const [pfp, setPfp] = useState(null); const formEl = useRef(null); useEffect(() => { async function fetchWorkspace() { const workspace = await Workspace.bySlug(slug); + const pfpUrl = await Workspace.fetchPfp(workspace.slug); + setPfp(pfpUrl); setWorkspace(workspace); setLoading(false); } fetchWorkspace(); }, [slug]); + const handleFileUpload = async (event) => { + const file = event.target.files[0]; + if (!file) return false; + + const formData = new FormData(); + formData.append("file", file); + const { success, error } = await Workspace.uploadPfp( + formData, + workspace.slug + ); + if (!success) { + showToast(`Failed to upload profile picture: ${error}`, "error"); + return; + } + + const pfpUrl = await Workspace.fetchPfp(workspace.slug); + setPfp(pfpUrl); + showToast("Profile picture uploaded.", "success"); + }; + + const handleRemovePfp = async () => { + const { success, error } = await Workspace.removePfp(workspace.slug); + if (!success) { + showToast(`Failed to remove profile picture: ${error}`, "error"); + return; + } + + setPfp(null); + }; + const handleUpdate = async (e) => { setSaving(true); e.preventDefault(); @@ -70,6 +104,47 @@ export default function GeneralInfo({ slug }) { + + {/* Image */} +
+
+ + {pfp && ( + + )} +
+
); } diff --git a/server/endpoints/system.js b/server/endpoints/system.js index a36777c8..74b83688 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -548,8 +548,6 @@ function systemEndpoints(app) { const userRecord = await User.get({ id: user.id }); const oldPfpFilename = userRecord.pfpFilename; - - console.log("oldPfpFilename", oldPfpFilename); if (oldPfpFilename) { const oldPfpPath = path.join( __dirname, diff --git a/server/endpoints/workspaces.js b/server/endpoints/workspaces.js index 54228bba..04263fd8 100644 --- a/server/endpoints/workspaces.js +++ b/server/endpoints/workspaces.js @@ -19,6 +19,15 @@ const { validWorkspaceSlug } = require("../utils/middleware/validWorkspace"); const { convertToChatHistory } = require("../utils/helpers/chat/responses"); const { CollectorApi } = require("../utils/collectorApi"); const { handleUploads } = setupMulter(); +const { setupPfpUploads } = require("../utils/files/multer"); +const { normalizePath } = require("../utils/files"); +const { handlePfpUploads } = setupPfpUploads(); +const path = require("path"); +const fs = require("fs"); +const { + determineWorkspacePfpFilepath, + fetchPfp, +} = require("../utils/files/pfp"); function workspaceEndpoints(app) { if (!app) return; @@ -422,6 +431,127 @@ function workspaceEndpoints(app) { } } ); + + app.get( + "/workspace/:slug/pfp", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async function (request, response) { + try { + const { slug } = request.params; + const pfpPath = await determineWorkspacePfpFilepath(slug); + + if (!pfpPath) { + response.sendStatus(204).end(); + return; + } + + const { found, buffer, size, mime } = fetchPfp(pfpPath); + if (!found) { + response.sendStatus(204).end(); + return; + } + + response.writeHead(200, { + "Content-Type": mime || "image/png", + "Content-Disposition": `attachment; filename=${path.basename( + pfpPath + )}`, + "Content-Length": size, + }); + response.end(Buffer.from(buffer, "base64")); + return; + } catch (error) { + console.error("Error processing the logo request:", error); + response.status(500).json({ message: "Internal server error" }); + } + } + ); + + app.post( + "/workspace/:slug/upload-pfp", + [validatedRequest, flexUserRoleValid([ROLES.all])], + handlePfpUploads.single("file"), + async function (request, response) { + try { + const { slug } = request.params; + const uploadedFileName = request.randomFileName; + if (!uploadedFileName) { + return response.status(400).json({ message: "File upload failed." }); + } + + const workspaceRecord = await Workspace.get({ + slug, + }); + + const oldPfpFilename = workspaceRecord.pfpFilename; + if (oldPfpFilename) { + const oldPfpPath = path.join( + __dirname, + `../storage/assets/pfp/${normalizePath( + workspaceRecord.pfpFilename + )}` + ); + + if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); + } + + const { workspace, message } = await Workspace.update( + workspaceRecord.id, + { + pfpFilename: uploadedFileName, + } + ); + + return response.status(workspace ? 200 : 500).json({ + message: workspace + ? "Profile picture uploaded successfully." + : message, + }); + } catch (error) { + console.error("Error processing the profile picture upload:", error); + response.status(500).json({ message: "Internal server error" }); + } + } + ); + + app.delete( + "/workspace/:slug/remove-pfp", + [validatedRequest, flexUserRoleValid([ROLES.all])], + async function (request, response) { + try { + const { slug } = request.params; + const workspaceRecord = await Workspace.get({ + slug, + }); + const oldPfpFilename = workspaceRecord.pfpFilename; + + if (oldPfpFilename) { + const oldPfpPath = path.join( + __dirname, + `../storage/assets/pfp/${normalizePath(oldPfpFilename)}` + ); + + if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath); + } + + const { workspace, message } = await Workspace.update( + workspaceRecord.id, + { + pfpFilename: null, + } + ); + + return response.status(workspace ? 200 : 500).json({ + message: workspace + ? "Profile picture removed successfully." + : message, + }); + } catch (error) { + console.error("Error processing the profile picture removal:", error); + response.status(500).json({ message: "Internal server error" }); + } + } + ); } module.exports = { workspaceEndpoints }; diff --git a/server/models/workspace.js b/server/models/workspace.js index 92c2f9e3..48952c63 100644 --- a/server/models/workspace.js +++ b/server/models/workspace.js @@ -19,6 +19,7 @@ const Workspace = { "chatModel", "topN", "chatMode", + "pfpFilename", ], new: async function (name = null, creatorId = null) { diff --git a/server/prisma/migrations/20240301002308_init/migration.sql b/server/prisma/migrations/20240301002308_init/migration.sql new file mode 100644 index 00000000..5847beaf --- /dev/null +++ b/server/prisma/migrations/20240301002308_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "workspaces" ADD COLUMN "pfpFilename" TEXT; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 8cd3a1d3..e6121e29 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -100,6 +100,7 @@ model workspaces { chatModel String? topN Int? @default(4) chatMode String? @default("chat") + pfpFilename String? workspace_users workspace_users[] documents workspace_documents[] workspace_suggested_messages workspace_suggested_messages[] diff --git a/server/utils/files/pfp.js b/server/utils/files/pfp.js index dd6ba0fe..0d1dd9f8 100644 --- a/server/utils/files/pfp.js +++ b/server/utils/files/pfp.js @@ -3,6 +3,7 @@ const fs = require("fs"); const { getType } = require("mime"); const { User } = require("../../models/user"); const { normalizePath } = require("."); +const { Workspace } = require("../../models/workspace"); function fetchPfp(pfpPath) { if (!fs.existsSync(pfpPath)) { @@ -38,7 +39,21 @@ async function determinePfpFilepath(id) { return pfpFilepath; } +async function determineWorkspacePfpFilepath(slug) { + const workspace = await Workspace.get({ slug }); + const pfpFilename = workspace?.pfpFilename || null; + if (!pfpFilename) return null; + + const basePath = process.env.STORAGE_DIR + ? path.join(process.env.STORAGE_DIR, "assets/pfp") + : path.join(__dirname, "../../storage/assets/pfp"); + const pfpFilepath = path.join(basePath, normalizePath(pfpFilename)); + if (!fs.existsSync(pfpFilepath)) return null; + return pfpFilepath; +} + module.exports = { fetchPfp, determinePfpFilepath, + determineWorkspacePfpFilepath, };