mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-16 11:20:10 +01:00
Add user PFP support and context to logo (#408)
* fix sizing of onboarding modals & lint * fix extra scrolling on mobile onboarding flow * added message to use desktop for onboarding * linting * add arrow to scroll to bottom (debounced) and fix chat scrolling to always scroll to very bottom on message history change * fix for empty chat * change mobile alert copy * WIP adding PFP upload support * WIP pfp for users * edit account menu complete with change username/password and upload profile picture * add pfp context to update all instances of usePfp hook on update * linting * add context for logo change to immediately update logo * fix div with bullet points to use list-disc instead * fix: small changes * update multer file storage locations * fix: use STORAGE_DIR for filepathing --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
f48e6b1a3e
commit
fcb591d364
@ -8,6 +8,8 @@ import PrivateRoute, {
|
|||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import Login from "@/pages/Login";
|
import Login from "@/pages/Login";
|
||||||
|
import { PfpProvider } from "./PfpContext";
|
||||||
|
import { LogoProvider } from "./LogoContext";
|
||||||
|
|
||||||
const Main = lazy(() => import("@/pages/Main"));
|
const Main = lazy(() => import("@/pages/Main"));
|
||||||
const InvitePage = lazy(() => import("@/pages/Invite"));
|
const InvitePage = lazy(() => import("@/pages/Invite"));
|
||||||
@ -40,6 +42,8 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<div />}>
|
<Suspense fallback={<div />}>
|
||||||
<ContextWrapper>
|
<ContextWrapper>
|
||||||
|
<LogoProvider>
|
||||||
|
<PfpProvider>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
<Route path="/" element={<PrivateRoute Component={Main} />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
@ -103,6 +107,8 @@ export default function App() {
|
|||||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
|
</PfpProvider>
|
||||||
|
</LogoProvider>
|
||||||
</ContextWrapper>
|
</ContextWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
28
frontend/src/LogoContext.jsx
Normal file
28
frontend/src/LogoContext.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createContext, useEffect, useState } from "react";
|
||||||
|
import AnythingLLM from "./media/logo/anything-llm.png";
|
||||||
|
import System from "./models/system";
|
||||||
|
|
||||||
|
export const LogoContext = createContext();
|
||||||
|
|
||||||
|
export function LogoProvider({ children }) {
|
||||||
|
const [logo, setLogo] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchInstanceLogo() {
|
||||||
|
try {
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
||||||
|
} catch (err) {
|
||||||
|
setLogo(AnythingLLM);
|
||||||
|
console.error("Failed to fetch logo:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchInstanceLogo();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogoContext.Provider value={{ logo, setLogo }}>
|
||||||
|
{children}
|
||||||
|
</LogoContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
30
frontend/src/PfpContext.jsx
Normal file
30
frontend/src/PfpContext.jsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import React, { createContext, useState, useEffect } from "react";
|
||||||
|
import useUser from "./hooks/useUser";
|
||||||
|
import System from "./models/system";
|
||||||
|
|
||||||
|
export const PfpContext = createContext();
|
||||||
|
|
||||||
|
export function PfpProvider({ children }) {
|
||||||
|
const [pfp, setPfp] = useState(null);
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchPfp() {
|
||||||
|
if (!user?.id) return;
|
||||||
|
try {
|
||||||
|
const pfpUrl = await System.fetchPfp(user.id);
|
||||||
|
setPfp(pfpUrl);
|
||||||
|
} catch (err) {
|
||||||
|
setPfp(null);
|
||||||
|
console.error("Failed to fetch pfp:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchPfp();
|
||||||
|
}, [user?.id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PfpContext.Provider value={{ pfp, setPfp }}>
|
||||||
|
{children}
|
||||||
|
</PfpContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,32 +1,35 @@
|
|||||||
import React, { useRef, useEffect } from "react";
|
import React, { useRef, useEffect } from "react";
|
||||||
import JAZZ from "@metamask/jazzicon";
|
import JAZZ from "@metamask/jazzicon";
|
||||||
|
import usePfp from "../../hooks/usePfp";
|
||||||
|
|
||||||
export default function Jazzicon({ size = 10, user, role }) {
|
export default function Jazzicon({ size = 10, user, role }) {
|
||||||
|
const { pfp } = usePfp();
|
||||||
const divRef = useRef(null);
|
const divRef = useRef(null);
|
||||||
const seed = user?.uid
|
const seed = user?.uid
|
||||||
? toPseudoRandomInteger(user.uid)
|
? toPseudoRandomInteger(user.uid)
|
||||||
: Math.floor(100000 + Math.random() * 900000);
|
: Math.floor(100000 + Math.random() * 900000);
|
||||||
const result = JAZZ(size, seed);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!divRef || !divRef.current) return null;
|
if (!divRef.current || (role === "user" && pfp)) return;
|
||||||
|
|
||||||
|
const result = JAZZ(size, seed);
|
||||||
divRef.current.appendChild(result);
|
divRef.current.appendChild(result);
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [pfp, role, seed, size]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="relative w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden">
|
||||||
className={`flex ${role === "user" ? "user-reply" : ""}`}
|
<div ref={divRef} />
|
||||||
ref={divRef}
|
{role === "user" && pfp && (
|
||||||
|
<img
|
||||||
|
src={pfp}
|
||||||
|
alt="User profile picture"
|
||||||
|
className="absolute top-0 left-0 w-full h-full object-cover rounded-full bg-white"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPseudoRandomInteger(uidString = "") {
|
function toPseudoRandomInteger(uidString = "") {
|
||||||
var numberArray = [uidString.length];
|
return uidString.split("").reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
||||||
for (var i = 0; i < uidString.length; i++) {
|
|
||||||
numberArray[i] = uidString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
return numberArray.reduce((a, b) => a + b, 0);
|
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,12 @@ import React, { useState, useEffect, useRef } from "react";
|
|||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
import paths from "@/utils/paths";
|
import paths from "@/utils/paths";
|
||||||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
|
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
|
||||||
import { Person, SignOut } from "@phosphor-icons/react";
|
import { Person, Plus, X } from "@phosphor-icons/react";
|
||||||
import { userFromStorage } from "@/utils/request";
|
import { userFromStorage } from "@/utils/request";
|
||||||
|
import useUser from "@/hooks/useUser";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import usePfp from "@/hooks/usePfp";
|
||||||
|
|
||||||
export default function UserMenu({ children }) {
|
export default function UserMenu({ children }) {
|
||||||
if (isMobile) return <>{children}</>;
|
if (isMobile) return <>{children}</>;
|
||||||
@ -26,12 +30,28 @@ function useLoginMode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function userDisplay() {
|
function userDisplay() {
|
||||||
|
const { pfp } = usePfp();
|
||||||
const user = userFromStorage();
|
const user = userFromStorage();
|
||||||
|
|
||||||
|
if (pfp) {
|
||||||
|
return (
|
||||||
|
<div className="w-[35px] h-[35px] rounded-full flex-shrink-0 overflow-hidden transition-all duration-300 bg-gray-100 hover:border-slate-100 hover:border-opacity-50 border-transparent border hover:opacity-60">
|
||||||
|
<img
|
||||||
|
src={pfp}
|
||||||
|
alt="User profile picture"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return user?.username?.slice(0, 2) || "AA";
|
return user?.username?.slice(0, 2) || "AA";
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserButton() {
|
function UserButton() {
|
||||||
|
const { user } = useUser();
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
|
const [showAccountSettings, setShowAccountSettings] = useState(false);
|
||||||
const mode = useLoginMode();
|
const mode = useLoginMode();
|
||||||
const menuRef = useRef();
|
const menuRef = useRef();
|
||||||
const buttonRef = useRef();
|
const buttonRef = useRef();
|
||||||
@ -45,6 +65,11 @@ function UserButton() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenAccountModal = () => {
|
||||||
|
setShowAccountSettings(true);
|
||||||
|
setShowMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showMenu) {
|
if (showMenu) {
|
||||||
document.addEventListener("mousedown", handleClose);
|
document.addEventListener("mousedown", handleClose);
|
||||||
@ -71,6 +96,14 @@ function UserButton() {
|
|||||||
className="w-fit rounded-lg absolute top-12 right-0 bg-sidebar p-4 flex items-center-justify-center"
|
className="w-fit rounded-lg absolute top-12 right-0 bg-sidebar p-4 flex items-center-justify-center"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{mode === "multi" && !!user && (
|
||||||
|
<button
|
||||||
|
onClick={handleOpenAccountModal}
|
||||||
|
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<a
|
<a
|
||||||
href={paths.mailToMintplex()}
|
href={paths.mailToMintplex()}
|
||||||
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
|
className="text-white hover:bg-slate-200/20 w-full text-left px-4 py-1.5 rounded-md"
|
||||||
@ -92,6 +125,178 @@ function UserButton() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{user && showAccountSettings && (
|
||||||
|
<AccountModal
|
||||||
|
user={user}
|
||||||
|
hideModal={() => setShowAccountSettings(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AccountModal({ user, hideModal }) {
|
||||||
|
const { pfp, setPfp } = usePfp();
|
||||||
|
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 System.uploadPfp(formData);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Failed to upload profile picture: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pfpUrl = await System.fetchPfp(user.id);
|
||||||
|
setPfp(pfpUrl);
|
||||||
|
|
||||||
|
showToast("Profile picture uploaded successfully.", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemovePfp = async () => {
|
||||||
|
const { success, error } = await System.removePfp();
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Failed to remove profile picture: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPfp(null);
|
||||||
|
showToast("Profile picture removed successfully.", "success");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
for (var [key, value] of form.entries()) {
|
||||||
|
if (!value || value === null) continue;
|
||||||
|
data[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, error } = await System.updateUser(data);
|
||||||
|
if (success) {
|
||||||
|
let storedUser = JSON.parse(localStorage.getItem(AUTH_USER));
|
||||||
|
|
||||||
|
if (storedUser) {
|
||||||
|
storedUser.username = data.username;
|
||||||
|
localStorage.setItem(AUTH_USER, JSON.stringify(storedUser));
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to update user: ${error}`, "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="account-modal"
|
||||||
|
className="bg-black/20 fixed top-0 left-0 outline-none w-screen h-screen flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="relative w-[500px] max-w-2xl max-h-full bg-main-gradient rounded-lg shadow">
|
||||||
|
<div className="flex items-start justify-between p-4 border-b rounded-t border-gray-500/50">
|
||||||
|
<h3 className="text-xl font-semibold text-white">Edit Account</h3>
|
||||||
|
<button
|
||||||
|
onClick={hideModal}
|
||||||
|
type="button"
|
||||||
|
className="text-gray-400 bg-transparent hover:border-white/60 rounded-lg p-1.5 ml-auto inline-flex items-center hover:bg-menu-item-selected-gradient hover:border-slate-100 border-transparent"
|
||||||
|
>
|
||||||
|
<X className="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form onSubmit={handleUpdate} className="space-y-6">
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-center gap-8">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<label className="w-48 h-48 flex flex-col items-center justify-center bg-zinc-900/50 transition-all duration-300 rounded-full mt-8 border-2 border-dashed border-white border-opacity-60 cursor-pointer hover:opacity-60">
|
||||||
|
<input
|
||||||
|
id="logo-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
{pfp ? (
|
||||||
|
<img
|
||||||
|
src={pfp}
|
||||||
|
alt="User profile picture"
|
||||||
|
className="w-48 h-48 rounded-full object-cover bg-white"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center p-3">
|
||||||
|
<Plus className="w-8 h-8 text-white/80 m-2" />
|
||||||
|
<span className="text-white text-opacity-80 text-sm font-semibold">
|
||||||
|
Profile Picture
|
||||||
|
</span>
|
||||||
|
<span className="text-white text-opacity-60 text-xs">
|
||||||
|
800 x 800
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
{pfp && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleRemovePfp}
|
||||||
|
className="mt-3 text-white text-opacity-60 text-sm font-medium hover:underline"
|
||||||
|
>
|
||||||
|
Remove Profile Picture
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-y-4 px-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="username"
|
||||||
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder="User's username"
|
||||||
|
minLength={2}
|
||||||
|
defaultValue={user.username}
|
||||||
|
required
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="password"
|
||||||
|
className="block mb-2 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder={`${user.username}'s new password`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center border-t border-gray-500/50 pt-4 p-6">
|
||||||
|
<button
|
||||||
|
onClick={hideModal}
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 rounded-lg text-white bg-transparent hover:bg-stone-900"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 rounded-lg text-white bg-transparent border border-slate-200 hover:bg-slate-200 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
Update Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useContext } from "react";
|
||||||
import System from "@/models/system";
|
import { LogoContext } from "../LogoContext";
|
||||||
import AnythingLLM from "@/media/logo/anything-llm.png";
|
|
||||||
|
|
||||||
export default function useLogo() {
|
export default function useLogo() {
|
||||||
const [logo, setLogo] = useState("");
|
const { logo, setLogo } = useContext(LogoContext);
|
||||||
|
return { logo, setLogo };
|
||||||
useEffect(() => {
|
|
||||||
async function fetchInstanceLogo() {
|
|
||||||
try {
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
|
||||||
} catch (err) {
|
|
||||||
setLogo(AnythingLLM);
|
|
||||||
console.error("Failed to fetch logo:", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetchInstanceLogo();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { logo };
|
|
||||||
}
|
}
|
||||||
|
7
frontend/src/hooks/usePfp.js
Normal file
7
frontend/src/hooks/usePfp.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { PfpContext } from "../PfpContext";
|
||||||
|
|
||||||
|
export default function usePfp() {
|
||||||
|
const { pfp, setPfp } = useContext(PfpContext);
|
||||||
|
return { pfp, setPfp };
|
||||||
|
}
|
@ -170,6 +170,21 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
uploadPfp: async function (formData) {
|
||||||
|
return await fetch(`${API_BASE}/system/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 };
|
||||||
|
});
|
||||||
|
},
|
||||||
uploadLogo: async function (formData) {
|
uploadLogo: async function (formData) {
|
||||||
return await fetch(`${API_BASE}/system/upload-logo`, {
|
return await fetch(`${API_BASE}/system/upload-logo`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -191,7 +206,7 @@ const System = {
|
|||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) return res.blob();
|
if (res.ok && res.status !== 204) return res.blob();
|
||||||
throw new Error("Failed to fetch logo!");
|
throw new Error("Failed to fetch logo!");
|
||||||
})
|
})
|
||||||
.then((blob) => URL.createObjectURL(blob))
|
.then((blob) => URL.createObjectURL(blob))
|
||||||
@ -200,6 +215,36 @@ const System = {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
fetchPfp: async function (id) {
|
||||||
|
return await fetch(`${API_BASE}/system/pfp/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
cache: "no-cache",
|
||||||
|
})
|
||||||
|
.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 (id) {
|
||||||
|
return await fetch(`${API_BASE}/system/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 };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
isDefaultLogo: async function () {
|
isDefaultLogo: async function () {
|
||||||
return await fetch(`${API_BASE}/system/is-default-logo`, {
|
return await fetch(`${API_BASE}/system/is-default-logo`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -374,6 +419,18 @@ const System = {
|
|||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateUser: async (data) => {
|
||||||
|
return await fetch(`${API_BASE}/system/user`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return { success: false, error: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default System;
|
export default System;
|
||||||
|
@ -10,7 +10,7 @@ import showToast from "@/utils/toast";
|
|||||||
import { Plus } from "@phosphor-icons/react";
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
|
||||||
export default function Appearance() {
|
export default function Appearance() {
|
||||||
const { logo: _initLogo } = useLogo();
|
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
|
||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
@ -49,6 +49,9 @@ export default function Appearance() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
showToast("Image uploaded successfully.", "success");
|
showToast("Image uploaded successfully.", "success");
|
||||||
setIsDefaultLogo(false);
|
setIsDefaultLogo(false);
|
||||||
};
|
};
|
||||||
@ -67,6 +70,9 @@ export default function Appearance() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
showToast("Image successfully removed.", "success");
|
showToast("Image successfully removed.", "success");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Plus } from "@phosphor-icons/react";
|
|||||||
import showToast from "@/utils/toast";
|
import showToast from "@/utils/toast";
|
||||||
|
|
||||||
function AppearanceSetup({ prevStep, nextStep }) {
|
function AppearanceSetup({ prevStep, nextStep }) {
|
||||||
const { logo: _initLogo } = useLogo();
|
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
|
||||||
const [logo, setLogo] = useState("");
|
const [logo, setLogo] = useState("");
|
||||||
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
||||||
|
|
||||||
@ -35,6 +35,9 @@ function AppearanceSetup({ prevStep, nextStep }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
showToast("Image uploaded successfully.", "success");
|
showToast("Image uploaded successfully.", "success");
|
||||||
setIsDefaultLogo(false);
|
setIsDefaultLogo(false);
|
||||||
};
|
};
|
||||||
@ -53,6 +56,9 @@ function AppearanceSetup({ prevStep, nextStep }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
showToast("Image successfully removed.", "success");
|
showToast("Image successfully removed.", "success");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,13 +16,18 @@ const {
|
|||||||
userFromSession,
|
userFromSession,
|
||||||
multiUserMode,
|
multiUserMode,
|
||||||
} = require("../utils/http");
|
} = require("../utils/http");
|
||||||
const { setupDataImports, setupLogoUploads } = require("../utils/files/multer");
|
const {
|
||||||
|
setupDataImports,
|
||||||
|
setupLogoUploads,
|
||||||
|
setupPfpUploads,
|
||||||
|
} = require("../utils/files/multer");
|
||||||
const { v4 } = require("uuid");
|
const { v4 } = require("uuid");
|
||||||
const { SystemSettings } = require("../models/systemSettings");
|
const { SystemSettings } = require("../models/systemSettings");
|
||||||
const { User } = require("../models/user");
|
const { User } = require("../models/user");
|
||||||
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
const { validatedRequest } = require("../utils/middleware/validatedRequest");
|
||||||
const { handleImports } = setupDataImports();
|
const { handleImports } = setupDataImports();
|
||||||
const { handleLogoUploads } = setupLogoUploads();
|
const { handleLogoUploads } = setupLogoUploads();
|
||||||
|
const { handlePfpUploads } = setupPfpUploads();
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const {
|
const {
|
||||||
@ -41,6 +46,7 @@ const { getCustomModels } = require("../utils/helpers/customModels");
|
|||||||
const { WorkspaceChats } = require("../models/workspaceChats");
|
const { WorkspaceChats } = require("../models/workspaceChats");
|
||||||
const { Workspace } = require("../models/workspace");
|
const { Workspace } = require("../models/workspace");
|
||||||
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
|
const { flexUserRoleValid } = require("../utils/middleware/multiUserProtected");
|
||||||
|
const { fetchPfp, determinePfpFilepath } = require("../utils/files/pfp");
|
||||||
|
|
||||||
function systemEndpoints(app) {
|
function systemEndpoints(app) {
|
||||||
if (!app) return;
|
if (!app) return;
|
||||||
@ -399,7 +405,12 @@ function systemEndpoints(app) {
|
|||||||
try {
|
try {
|
||||||
const defaultFilename = getDefaultFilename();
|
const defaultFilename = getDefaultFilename();
|
||||||
const logoPath = await determineLogoFilepath(defaultFilename);
|
const logoPath = await determineLogoFilepath(defaultFilename);
|
||||||
const { buffer, size, mime } = fetchLogo(logoPath);
|
const { found, buffer, size, mime } = fetchLogo(logoPath);
|
||||||
|
if (!found) {
|
||||||
|
response.sendStatus(204).end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
response.writeHead(200, {
|
response.writeHead(200, {
|
||||||
"Content-Type": mime || "image/png",
|
"Content-Type": mime || "image/png",
|
||||||
"Content-Disposition": `attachment; filename=${path.basename(
|
"Content-Disposition": `attachment; filename=${path.basename(
|
||||||
@ -415,6 +426,110 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/system/pfp/:id", async function (request, response) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const pfpPath = await determinePfpFilepath(id);
|
||||||
|
|
||||||
|
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(
|
||||||
|
"/system/upload-pfp",
|
||||||
|
[validatedRequest, flexUserRoleValid],
|
||||||
|
handlePfpUploads.single("file"),
|
||||||
|
async function (request, response) {
|
||||||
|
try {
|
||||||
|
const user = await userFromSession(request, response);
|
||||||
|
const uploadedFileName = request.randomFileName;
|
||||||
|
|
||||||
|
if (!uploadedFileName) {
|
||||||
|
return response.status(400).json({ message: "File upload failed." });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRecord = await User.get({ id: user.id });
|
||||||
|
const oldPfpFilename = userRecord.pfpFilename;
|
||||||
|
console.log("oldPfpFilename", oldPfpFilename);
|
||||||
|
if (oldPfpFilename) {
|
||||||
|
const oldPfpPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
`../storage/assets/pfp/${oldPfpFilename}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, error } = await User.update(user.id, {
|
||||||
|
pfpFilename: uploadedFileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.status(success ? 200 : 500).json({
|
||||||
|
message: success
|
||||||
|
? "Profile picture uploaded successfully."
|
||||||
|
: error || "Failed to update with new profile picture.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing the profile picture upload:", error);
|
||||||
|
response.status(500).json({ message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.delete(
|
||||||
|
"/system/remove-pfp",
|
||||||
|
[validatedRequest, flexUserRoleValid],
|
||||||
|
async function (request, response) {
|
||||||
|
try {
|
||||||
|
const user = await userFromSession(request, response);
|
||||||
|
const userRecord = await User.get({ id: user.id });
|
||||||
|
const oldPfpFilename = userRecord.pfpFilename;
|
||||||
|
console.log("oldPfpFilename", oldPfpFilename);
|
||||||
|
if (oldPfpFilename) {
|
||||||
|
const oldPfpPath = path.join(
|
||||||
|
__dirname,
|
||||||
|
`../storage/assets/pfp/${oldPfpFilename}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(oldPfpPath)) fs.unlinkSync(oldPfpPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, error } = await User.update(user.id, {
|
||||||
|
pfpFilename: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.status(success ? 200 : 500).json({
|
||||||
|
message: success
|
||||||
|
? "Profile picture removed successfully."
|
||||||
|
: error || "Failed to remove profile picture.",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing the profile picture removal:", error);
|
||||||
|
response.status(500).json({ message: "Internal server error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
app.post(
|
app.post(
|
||||||
"/system/upload-logo",
|
"/system/upload-logo",
|
||||||
[validatedRequest, flexUserRoleValid],
|
[validatedRequest, flexUserRoleValid],
|
||||||
@ -738,6 +853,40 @@ function systemEndpoints(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.post("/system/user", [validatedRequest], async (request, response) => {
|
||||||
|
try {
|
||||||
|
const sessionUser = await userFromSession(request, response);
|
||||||
|
const { username, password } = reqBody(request);
|
||||||
|
const id = Number(sessionUser.id);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
response.status(400).json({ success: false, error: "Invalid user ID" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
if (username) {
|
||||||
|
updates.username = username;
|
||||||
|
}
|
||||||
|
if (password) {
|
||||||
|
updates.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updates).length === 0) {
|
||||||
|
response
|
||||||
|
.status(400)
|
||||||
|
.json({ success: false, error: "No updates provided" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { success, error } = await User.update(id, updates);
|
||||||
|
response.status(200).json({ success, error });
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { systemEndpoints };
|
module.exports = { systemEndpoints };
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "pfpFilename" TEXT;
|
@ -57,6 +57,7 @@ model users {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
username String? @unique
|
username String? @unique
|
||||||
password String
|
password String
|
||||||
|
pfpFilename String?
|
||||||
role String @default("default")
|
role String @default("default")
|
||||||
suspended Int @default(0)
|
suspended Int @default(0)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@ -41,6 +41,7 @@ function fetchLogo(logoPath) {
|
|||||||
const mime = getType(logoPath);
|
const mime = getType(logoPath);
|
||||||
const buffer = fs.readFileSync(logoPath);
|
const buffer = fs.readFileSync(logoPath);
|
||||||
return {
|
return {
|
||||||
|
found: true,
|
||||||
buffer,
|
buffer,
|
||||||
size: buffer.length,
|
size: buffer.length,
|
||||||
mime,
|
mime,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
|
const { v4 } = require("uuid");
|
||||||
|
|
||||||
function setupMulter() {
|
function setupMulter() {
|
||||||
// Handle File uploads for auto-uploading.
|
// Handle File uploads for auto-uploading.
|
||||||
@ -40,7 +41,10 @@ function setupLogoUploads() {
|
|||||||
// Handle Logo uploads.
|
// Handle Logo uploads.
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: function (_, _, cb) {
|
destination: function (_, _, cb) {
|
||||||
const uploadOutput = path.resolve(__dirname, `../../storage/assets`);
|
const uploadOutput =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? path.resolve(__dirname, `../../storage/assets`)
|
||||||
|
: path.resolve(process.env.STORAGE_DIR, "assets");
|
||||||
fs.mkdirSync(uploadOutput, { recursive: true });
|
fs.mkdirSync(uploadOutput, { recursive: true });
|
||||||
return cb(null, uploadOutput);
|
return cb(null, uploadOutput);
|
||||||
},
|
},
|
||||||
@ -52,8 +56,29 @@ function setupLogoUploads() {
|
|||||||
return { handleLogoUploads: multer({ storage }) };
|
return { handleLogoUploads: multer({ storage }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupPfpUploads() {
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (_, _, cb) {
|
||||||
|
const uploadOutput =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? path.resolve(__dirname, `../../storage/assets/pfp`)
|
||||||
|
: path.resolve(process.env.STORAGE_DIR, "assets/pfp");
|
||||||
|
fs.mkdirSync(uploadOutput, { recursive: true });
|
||||||
|
return cb(null, uploadOutput);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
const randomFileName = `${v4()}${path.extname(file.originalname)}`;
|
||||||
|
req.randomFileName = randomFileName;
|
||||||
|
cb(null, randomFileName);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { handlePfpUploads: multer({ storage }) };
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
setupMulter,
|
setupMulter,
|
||||||
setupDataImports,
|
setupDataImports,
|
||||||
setupLogoUploads,
|
setupLogoUploads,
|
||||||
|
setupPfpUploads,
|
||||||
};
|
};
|
||||||
|
44
server/utils/files/pfp.js
Normal file
44
server/utils/files/pfp.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const fs = require("fs");
|
||||||
|
const { getType } = require("mime");
|
||||||
|
const { User } = require("../../models/user");
|
||||||
|
|
||||||
|
function fetchPfp(pfpPath) {
|
||||||
|
if (!fs.existsSync(pfpPath)) {
|
||||||
|
return {
|
||||||
|
found: false,
|
||||||
|
buffer: null,
|
||||||
|
size: 0,
|
||||||
|
mime: "none/none",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mime = getType(pfpPath);
|
||||||
|
const buffer = fs.readFileSync(pfpPath);
|
||||||
|
return {
|
||||||
|
found: true,
|
||||||
|
buffer,
|
||||||
|
size: buffer.length,
|
||||||
|
mime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function determinePfpFilepath(id) {
|
||||||
|
const numberId = Number(id);
|
||||||
|
const user = await User.get({ id: numberId });
|
||||||
|
const pfpFilename = user.pfpFilename;
|
||||||
|
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, pfpFilename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(pfpFilepath)) return null;
|
||||||
|
return pfpFilepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
fetchPfp,
|
||||||
|
determinePfpFilepath,
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user