Logo customization for single-user & multi-user modes (#186)

* implemented logo customization for single-user mode

* removing unneeded comments

* added dark and light mode support for default logo

* implemented dark and light mode switching in frontend

* fixed dark and light mode switching for failed to load logo from backend

* removed unneeded comment

* custom logos for admin implemented

* refactor logo mgmt functions
abstract logo management utils into their own file for simplicity

* added settings tab for appearance on single-user mode

* unchecking files with unneeded changes

* fixed appearance settings tab to be hidden on multiuser mode

* allow readall for logo

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2023-08-14 15:22:55 -07:00 committed by GitHub
parent 16b8330fbf
commit b557bb9ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 679 additions and 32 deletions

View File

@ -11,6 +11,8 @@ const AdminInvites = lazy(() => import("./pages/Admin/Invitations"));
const AdminWorkspaces = lazy(() => import("./pages/Admin/Workspaces"));
const AdminChats = lazy(() => import("./pages/Admin/Chats"));
const AdminSystem = lazy(() => import("./pages/Admin/System"));
const AdminAppearance = lazy(() => import("./pages/Admin/Appearance"));
const Appearance = lazy(() => import("./pages/System/Appearance"));
export default function App() {
return (
@ -18,6 +20,7 @@ export default function App() {
<ContextWrapper>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/system/appearance" element={<Appearance />} />
<Route
path="/workspace/:slug"
element={<PrivateRoute Component={WorkspaceChat} />}
@ -45,6 +48,10 @@ export default function App() {
path="/admin/workspace-chats"
element={<AdminRoute Component={AdminChats} />}
/>
<Route
path="/admin/appearance"
element={<AdminRoute Component={AdminAppearance} />}
/>
</Routes>
</ContextWrapper>
</Suspense>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from "react";
import {
BookOpen,
Database,
Eye,
GitHub,
Mail,
Menu,
@ -14,9 +14,12 @@ import IndexCount from "../Sidebar/IndexCount";
import LLMStatus from "../Sidebar/LLMStatus";
import paths from "../../utils/paths";
import Discord from "../Icons/Discord";
import useLogo from "../../hooks/useLogo";
export default function AdminSidebar() {
const { logo } = useLogo();
const sidebarRef = useRef(null);
return (
<>
<div
@ -27,9 +30,14 @@ export default function AdminSidebar() {
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM Admin
</p>
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<a
href={paths.home()}
@ -69,6 +77,11 @@ export default function AdminSidebar() {
btnText="Workspace Chat Management"
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.appearance()}
btnText="Appearance"
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
/>
</div>
</div>
<div>
@ -117,6 +130,7 @@ export default function AdminSidebar() {
}
export function SidebarMobileHeader() {
const { logo } = useLogo();
const sidebarRef = useRef(null);
const [showSidebar, setShowSidebar] = useState(false);
const [showBgOverlay, setShowBgOverlay] = useState(false);
@ -143,9 +157,14 @@ export function SidebarMobileHeader() {
>
<Menu className="h-6 w-6" />
</button>
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM
</p>
<div className="flex shrink-0 w-fit items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded w-full max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
</div>
<div
style={{
@ -168,9 +187,14 @@ export function SidebarMobileHeader() {
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM Admin
</p>
<div className="flex shrink-0 w-fit items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded w-full max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<a
href={paths.home()}
@ -188,11 +212,36 @@ export function SidebarMobileHeader() {
style={{ height: "calc(100vw - -3rem)" }}
className=" flex flex-col gap-y-4 pb-8 overflow-y-scroll no-scroll"
>
<Option
href={paths.admin.system()}
btnText="System Preferences"
icon={<Settings className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.invites()}
btnText="Invitation Management"
icon={<Mail className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.users()}
btnText="User Management"
icon={<Users className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.workspaces()}
btnText="Workspace Management"
icon={<BookOpen className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.chats()}
btnText="Workspace Chat Management"
icon={<MessageSquare className="h-4 w-4 flex-shrink-0" />}
/>
<Option
href={paths.admin.appearance()}
btnText="Appearance"
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
/>
</div>
</div>
<div>

View File

@ -6,6 +6,7 @@ import {
Users,
Database,
MessageSquare,
Eye,
} from "react-feather";
import ExportOrImportData from "./ExportImport";
import PasswordProtection from "./PasswordProtection";
@ -14,6 +15,7 @@ import MultiUserMode from "./MultiUserMode";
import useUser from "../../../hooks/useUser";
import VectorDBSelection from "./VectorDbs";
import LLMSelection from "./LLMSelection";
import paths from "../../../utils/paths";
const TABS = {
llm: LLMSelection,
@ -130,6 +132,12 @@ function SettingTabs({ selectedTab, changeTab, settings, user }) {
icon={<Lock className="h-4 w-4 flex-shrink-0" />}
onClick={changeTab}
/>
<SettingTab
displayName="Appearance"
tabName="appearance"
icon={<Eye className="h-4 w-4 flex-shrink-0" />}
onClick={() => window.open(paths.appearance())}
/>
</>
)}
</ul>

View File

@ -26,8 +26,10 @@ import Discord from "../Icons/Discord";
import useUser from "../../hooks/useUser";
import { userFromStorage } from "../../utils/request";
import { AUTH_TOKEN, AUTH_USER } from "../../utils/constants";
import useLogo from "../../hooks/useLogo";
export default function Sidebar() {
const { logo } = useLogo();
const sidebarRef = useRef(null);
const {
showing: showingSystemSettingsModal,
@ -50,9 +52,14 @@ export default function Sidebar() {
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM
</p>
<div className="flex shrink-0 max-w-[50%] items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<AdminHome />
<button
@ -144,6 +151,7 @@ export default function Sidebar() {
}
export function SidebarMobileHeader() {
const { logo } = useLogo();
const sidebarRef = useRef(null);
const [showSidebar, setShowSidebar] = useState(false);
const [showBgOverlay, setShowBgOverlay] = useState(false);
@ -180,9 +188,14 @@ export function SidebarMobileHeader() {
>
<Menu className="h-6 w-6" />
</button>
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM
</p>
<div className="flex shrink-0 w-fit items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded w-full max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
</div>
<div
style={{
@ -205,9 +218,14 @@ export function SidebarMobileHeader() {
<div className="w-full h-full flex flex-col overflow-x-hidden items-between">
{/* Header Information */}
<div className="flex w-full items-center justify-between">
<p className="text-xl font-base text-slate-600 dark:text-slate-200">
AnythingLLM
</p>
<div className="flex shrink-0 w-fit items-center justify-start">
<img
src={logo}
alt="Logo"
className="rounded w-full max-h-[40px]"
style={{ objectFit: "contain" }}
/>
</div>
<div className="flex gap-x-2 items-center text-slate-500">
<AdminHome />
<button

View File

@ -0,0 +1,25 @@
import { useEffect, useState } from "react";
import usePrefersDarkMode from "./usePrefersDarkMode";
import System from "../models/system";
import AnythingLLMDark from "../media/logo/anything-llm-dark.png";
import AnythingLLMLight from "../media/logo/anything-llm-light.png";
export default function useLogo() {
const [logo, setLogo] = useState("");
const prefersDarkMode = usePrefersDarkMode();
useEffect(() => {
async function fetchInstanceLogo() {
try {
const logoURL = await System.fetchLogo(!prefersDarkMode);
setLogo(logoURL);
} catch (err) {
setLogo(prefersDarkMode ? AnythingLLMLight : AnythingLLMDark);
console.error("Failed to fetch logo:", err);
}
}
fetchInstanceLogo();
}, [prefersDarkMode]);
return { logo };
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -188,6 +188,34 @@ const Admin = {
return { success: false, error: e.message };
});
},
uploadLogo: async function (formData) {
return await fetch(`${API_BASE}/system/upload-logo`, {
method: "POST",
body: formData,
headers: baseHeaders(),
})
.then((res) => {
if (!res.ok) throw new Error("Error uploading logo.");
return { success: true, error: null };
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
removeCustomLogo: async function () {
return await fetch(`${API_BASE}/system/remove-logo`, {
headers: baseHeaders(),
})
.then((res) => {
if (res.ok) return { success: true, error: null };
throw new Error("Error removing logo!");
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
};
export default Admin;

View File

@ -147,6 +147,46 @@ const System = {
return { success: false, error: e.message };
});
},
uploadLogo: async function (formData) {
return await fetch(`${API_BASE}/system/upload-logo`, {
method: "POST",
body: formData,
})
.then((res) => {
if (!res.ok) throw new Error("Error uploading logo.");
return { success: true, error: null };
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
fetchLogo: async function (light = false) {
return await fetch(`${API_BASE}/system/logo${light ? "/light" : ""}`, {
method: "GET",
cache: "no-cache",
})
.then((res) => {
if (res.ok) return res.blob();
throw new Error("Failed to fetch logo!");
})
.then((blob) => URL.createObjectURL(blob))
.catch((e) => {
console.log(e);
return null;
});
},
removeCustomLogo: async function () {
return await fetch(`${API_BASE}/system/remove-logo`)
.then((res) => {
if (res.ok) return { success: true, error: null };
throw new Error("Error removing logo!");
})
.catch((e) => {
console.log(e);
return { success: false, error: e.message };
});
},
};
export default System;

View File

@ -0,0 +1,128 @@
import React, { useState, useEffect } from "react";
import Sidebar, { SidebarMobileHeader } from "../../../components/AdminSidebar";
import { isMobile } from "react-device-detect";
import Admin from "../../../models/admin";
import AnythingLLMLight from "../../../media/logo/anything-llm-light.png";
import AnythingLLMDark from "../../../media/logo/anything-llm-dark.png";
import usePrefersDarkMode from "../../../hooks/usePrefersDarkMode";
import useLogo from "../../../hooks/useLogo";
import System from "../../../models/system";
export default function Appearance() {
const { logo: _initLogo } = useLogo();
const [logo, setLogo] = useState("");
const prefersDarkMode = usePrefersDarkMode();
const [errorMsg, setErrorMsg] = useState("");
useEffect(() => {
async function setInitLogo() {
setLogo(_initLogo || "");
}
setInitLogo();
}, [_initLogo]);
useEffect(() => {
if (!!errorMsg) {
setTimeout(() => {
setErrorMsg("");
}, 3_500);
}
}, [errorMsg]);
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return false;
const formData = new FormData();
formData.append("logo", file);
const { success, error } = await Admin.uploadLogo(formData);
if (!success) {
setErrorMsg(error);
return;
}
const logoURL = await System.fetchLogo();
setLogo(logoURL);
setErrorMsg("");
window.location.reload();
};
const handleRemoveLogo = async () => {
const { success, error } = await Admin.removeCustomLogo();
if (!success) {
console.error("Failed to remove logo:", error);
setErrorMsg(error);
return;
}
const logoURL = await System.fetchLogo();
setLogo(logoURL);
setErrorMsg("");
window.location.reload();
};
return (
<div className="w-screen h-screen overflow-hidden bg-orange-100 dark:bg-stone-700 flex">
{!isMobile && <Sidebar />}
<div
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
className="transition-all duration-500 relative md:ml-[2px] md:mr-[8px] md:my-[16px] md:rounded-[26px] bg-white dark:bg-black-900 md:min-w-[82%] p-[18px] h-full overflow-y-scroll"
>
{isMobile && <SidebarMobileHeader />}
<div className="px-1 md:px-8">
<div className="mb-6">
<p className="text-3xl font-semibold text-slate-600 dark:text-slate-200">
Appearance Settings
</p>
<p className="mt-2 text-sm font-base text-slate-600 dark:text-slate-200">
Customize the appearance settings of your platform.
</p>
</div>
<div className="flex items-center">
<img
src={logo}
alt="Uploaded Logo"
className="w-48 h-48 object-contain mr-6"
onError={(e) =>
(e.target.src = prefersDarkMode
? AnythingLLMLight
: AnythingLLMDark)
}
/>
<div className="flex flex-col">
<div className="mb-4">
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Upload Image
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
</label>
<button
onClick={handleRemoveLogo}
className="ml-4 cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
>
Remove Custom Logo
</button>
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">
Upload your logo. Recommended size: 800x200.
</div>
</div>
</div>
{errorMsg && (
<div className="mt-4 text-sm text-red-600 dark:text-red-400 text-center">
{errorMsg}
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,134 @@
import React, { useState, useEffect } from "react";
import AnythingLLMLight from "../../media/logo/anything-llm-light.png";
import AnythingLLMDark from "../../media/logo/anything-llm-dark.png";
import System from "../../models/system";
import usePrefersDarkMode from "../../hooks/usePrefersDarkMode";
import useLogo from "../../hooks/useLogo";
export default function Appearance() {
const { logo: _initLogo } = useLogo();
const prefersDarkMode = usePrefersDarkMode();
const [logo, setLogo] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
async function setInitLogo() {
setLogo(_initLogo || "");
}
setInitLogo();
}, [_initLogo]);
useEffect(() => {
if (!!successMsg) {
setTimeout(() => {
setSuccessMsg("");
}, 3_500);
}
if (!!errorMsg) {
setTimeout(() => {
setErrorMsg("");
}, 3_500);
}
}, [successMsg, errorMsg]);
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return false;
const formData = new FormData();
formData.append("logo", file);
const { success, error } = await System.uploadLogo(formData);
if (!success) {
console.error("Failed to upload logo:", error);
setErrorMsg(error);
setSuccessMsg("");
return;
}
const logoURL = await System.fetchLogo();
setLogo(logoURL);
setSuccessMsg("Image uploaded successfully");
setErrorMsg("");
};
const handleRemoveLogo = async () => {
const { success, error } = await System.removeCustomLogo();
if (!success) {
console.error("Failed to remove logo:", error);
setErrorMsg(error);
setSuccessMsg("");
return;
}
const logoURL = await System.fetchLogo();
setLogo(logoURL);
setSuccessMsg("Image successfully removed");
setErrorMsg("");
};
return (
<div className="min-h-screen flex items-center justify-center bg-orange-100 dark:bg-black-900">
<div className="p-6 w-full max-w-xl bg-white dark:bg-stone-600 rounded-xl shadow-md space-y-4">
<h2 className="text-2xl font-bold text-center text-black dark:text-white">
Customize Appearance
</h2>
<p className="text-center text-xs font-light text-black dark:text-white">
Customize the logo you see on the sidebar
</p>
<div className="flex flex-col items-center border border-slate-200 dark:border-black-900 p-6 rounded-xl">
<img
src={logo}
alt="Uploaded Logo"
className="w-48 h-48 object-contain"
onError={(e) =>
(e.target.src = prefersDarkMode
? AnythingLLMLight
: AnythingLLMDark)
}
/>
<div className="flex gap-2 p-2 flex-col items-center">
<div className="text-sm text-gray-600 dark:text-gray-300">
Upload your logo
</div>
<div className="text-sm text-gray-600 dark:text-gray-300">
Recommended size at least 800x200
</div>
</div>
</div>
<div className="flex justify-center mt-4 gap-2">
<label className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Upload Image
<input
type="file"
accept="image/*"
className="hidden"
onChange={handleFileUpload}
/>
</label>
<button
onClick={handleRemoveLogo}
className="cursor-pointer text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-blue-300 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600"
>
Remove Custom Logo
</button>
</div>
{errorMsg && (
<div className="text-sm text-red-600 dark:text-red-400 text-center">
{errorMsg}
</div>
)}
{successMsg && (
<div className="text-sm text-green-600 dark:text-green-400 text-center">
{successMsg}
</div>
)}
</div>
</div>
);
}

View File

@ -22,6 +22,9 @@ export default {
feedback: () => {
return "https://mintplexlabs.typeform.com/to/i0KE3aEW";
},
appearance: () => {
return "/system/appearance";
},
workspace: {
chat: (slug) => {
return `/workspace/${slug}`;
@ -46,5 +49,8 @@ export default {
chats: () => {
return "/admin/workspace-chats";
},
appearance: () => {
return "/admin/appearance";
},
},
};

3
server/.gitignore vendored
View File

@ -1,5 +1,8 @@
.env.production
.env.development
storage/assets/*
!storage/assets/anything-llm-dark.png
!storage/assets/anything-llm-light.png
storage/documents/*
storage/vector-cache/*.json
storage/exports

View File

@ -8,6 +8,8 @@ const { WorkspaceChats } = require("../models/workspaceChats");
const { getVectorDbClass } = require("../utils/helpers");
const { userFromSession, reqBody } = require("../utils/http");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { setupLogoUploads } = require("../utils/files/multer");
const { handleLogoUploads } = setupLogoUploads();
function adminEndpoints(app) {
if (!app) return;

View File

@ -17,12 +17,23 @@ const {
userFromSession,
multiUserMode,
} = require("../utils/http");
const { setupDataImports } = require("../utils/files/multer");
const { setupDataImports, setupLogoUploads } = require("../utils/files/multer");
const { v4 } = require("uuid");
const { SystemSettings } = require("../models/systemSettings");
const { User } = require("../models/user");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
const { handleImports } = setupDataImports();
const { handleLogoUploads } = setupLogoUploads();
const path = require("path");
const {
getDefaultFilename,
determineLogoFilepath,
fetchLogo,
validFilename,
renameLogoFile,
removeCustomLogo,
DARK_LOGO_FILENAME,
} = require("../utils/files/logo");
function systemEndpoints(app) {
if (!app) return;
@ -358,6 +369,99 @@ function systemEndpoints(app) {
response.status(200).json({ success, error });
}
);
app.get("/system/logo/:mode?", async function (request, response) {
try {
const defaultFilename = getDefaultFilename(request.params.mode);
const logoPath = await determineLogoFilepath(defaultFilename);
const { buffer, size, mime } = fetchLogo(logoPath);
response.writeHead(200, {
"Content-Type": mime || "image/png",
"Content-Disposition": `attachment; filename=${path.basename(
logoPath
)}`,
"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(
"/system/upload-logo",
[validatedRequest],
handleLogoUploads.single("logo"),
async (request, response) => {
if (!request.file || !request.file.originalname) {
return response.status(400).json({ message: "No logo file provided." });
}
if (!validFilename(request.file.originalname)) {
return response.status(400).json({
message: "Invalid file name. Please choose a different file.",
});
}
try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const newFilename = await renameLogoFile(request.file.originalname);
const existingLogoFilename = await SystemSettings.currentLogoFilename();
await removeCustomLogo(existingLogoFilename);
const { success, error } = await SystemSettings.updateSettings({
logo_filename: newFilename,
});
return response.status(success ? 200 : 500).json({
message: success
? "Logo uploaded successfully."
: error || "Failed to update with new logo.",
});
} catch (error) {
console.error("Error processing the logo upload:", error);
response.status(500).json({ message: "Error uploading the logo." });
}
}
);
app.get(
"/system/remove-logo",
[validatedRequest],
async (request, response) => {
try {
if (
response.locals.multiUserMode &&
response.locals.user?.role !== "admin"
) {
return response.sendStatus(401).end();
}
const currentLogoFilename = await SystemSettings.currentLogoFilename();
await removeCustomLogo(currentLogoFilename);
const { success, error } = await SystemSettings.updateSettings({
logo_filename: DARK_LOGO_FILENAME,
});
return response.status(success ? 200 : 500).json({
message: success
? "Logo removed successfully."
: error || "Failed to update with new logo.",
});
} catch (error) {
console.error("Error processing the logo removal:", error);
response.status(500).json({ message: "Error removing the logo." });
}
}
);
}
module.exports = { systemEndpoints };

View File

@ -4,6 +4,7 @@ const SystemSettings = {
"users_can_delete_workspaces",
"limit_user_messages",
"message_limit",
"logo_filename",
],
privateField: [],
tablename: "system_settings",
@ -117,6 +118,10 @@ const SystemSettings = {
isMultiUserMode: async function () {
return (await this.get(`label = 'multi_user_mode'`))?.value === "true";
},
currentLogoFilename: async function () {
const result = await this.get(`label = 'logo_filename'`);
return result ? result.value : null;
},
};
module.exports.SystemSettings = SystemSettings;

View File

@ -30,6 +30,7 @@
"graphql": "^16.7.1",
"jsonwebtoken": "^8.5.1",
"langchain": "^0.0.90",
"mime": "^3.0.0",
"moment": "^2.29.4",
"multer": "^1.4.5-lts.1",
"openai": "^3.2.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,72 @@
const path = require("path");
const fs = require("fs");
const { getType } = require("mime");
const { v4 } = require("uuid");
const { SystemSettings } = require("../../models/systemSettings");
const LIGHT_LOGO_FILENAME = "anything-llm-light.png";
const DARK_LOGO_FILENAME = "anything-llm-dark.png";
function validFilename(newFilename = "") {
return ![DARK_LOGO_FILENAME, LIGHT_LOGO_FILENAME].includes(newFilename);
}
function getDefaultFilename(mode = "dark") {
return mode === "light" ? DARK_LOGO_FILENAME : LIGHT_LOGO_FILENAME;
}
async function determineLogoFilepath(defaultFilename = DARK_LOGO_FILENAME) {
const currentLogoFilename = await SystemSettings.currentLogoFilename();
const basePath = path.join(__dirname, "../../storage/assets");
const defaultFilepath = path.join(basePath, defaultFilename);
if (currentLogoFilename && validFilename(currentLogoFilename)) {
customLogoPath = path.join(basePath, currentLogoFilename);
return fs.existsSync(customLogoPath) ? customLogoPath : defaultFilepath;
}
return defaultFilepath;
}
function fetchLogo(logoPath) {
const mime = getType(logoPath);
const buffer = fs.readFileSync(logoPath);
return {
buffer,
size: buffer.length,
mime,
};
}
async function renameLogoFile(originalFilename = null) {
const extname = path.extname(originalFilename) || ".png";
const newFilename = `${v4()}${extname}`;
const originalFilepath = path.join(
__dirname,
`../../storage/assets/${originalFilename}`
);
const outputFilepath = path.join(
__dirname,
`../../storage/assets/${newFilename}`
);
fs.renameSync(originalFilepath, outputFilepath);
return newFilename;
}
async function removeCustomLogo(logoFilename = DARK_LOGO_FILENAME) {
if (!logoFilename || !validFilename(logoFilename)) return false;
const logoPath = path.join(__dirname, `../../storage/assets/${logoFilename}`);
if (fs.existsSync(logoPath)) fs.unlinkSync(logoPath);
return true;
}
module.exports = {
fetchLogo,
renameLogoFile,
removeCustomLogo,
validFilename,
getDefaultFilename,
determineLogoFilepath,
LIGHT_LOGO_FILENAME,
DARK_LOGO_FILENAME,
};

View File

@ -1,9 +1,11 @@
const multer = require("multer");
const path = require("path");
const fs = require("fs");
function setupMulter() {
const multer = require("multer");
// Handle File uploads for auto-uploading.
const storage = multer.diskStorage({
destination: function (_, _, cb) {
const path = require("path");
const uploadOutput =
process.env.NODE_ENV === "development"
? path.resolve(__dirname, `../../../collector/hotdir`)
@ -14,19 +16,14 @@ function setupMulter() {
cb(null, file.originalname);
},
});
const upload = multer({
storage,
});
return { handleUploads: upload };
return { handleUploads: multer({ storage }) };
}
function setupDataImports() {
const multer = require("multer");
// Handle File uploads for auto-uploading.
const storage = multer.diskStorage({
destination: function (_, _, cb) {
const path = require("path");
const fs = require("fs");
const uploadOutput = path.resolve(__dirname, `../../storage/imports`);
fs.mkdirSync(uploadOutput, { recursive: true });
return cb(null, uploadOutput);
@ -35,13 +32,28 @@ function setupDataImports() {
cb(null, file.originalname);
},
});
const upload = multer({
storage,
return { handleImports: multer({ storage }) };
}
function setupLogoUploads() {
// Handle Logo uploads.
const storage = multer.diskStorage({
destination: function (_, _, cb) {
const uploadOutput = path.resolve(__dirname, `../../storage/assets`);
fs.mkdirSync(uploadOutput, { recursive: true });
return cb(null, uploadOutput);
},
filename: function (_, file, cb) {
cb(null, file.originalname);
},
});
return { handleImports: upload };
return { handleLogoUploads: multer({ storage }) };
}
module.exports = {
setupMulter,
setupDataImports,
setupLogoUploads,
};

View File

@ -1648,6 +1648,11 @@ mime@1.6.0:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
mime@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7"
integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==
minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"