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,
};