mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-11-10 17:00:11 +01:00
[FEAT] Custom login screen icon + custom app name (#1500)
* implement custom icon on login screen for single & multi user + custom app name feature * hide field when not relevant * set customApp name * show original anythingllm login logo unless custom logo is set * nit-picks * remove console log --------- Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
parent
a89812703b
commit
6a2d7aca28
@ -1,27 +1,41 @@
|
||||
import { createContext, useEffect, useState } from "react";
|
||||
import AnythingLLM from "./media/logo/anything-llm.png";
|
||||
import DefaultLoginLogo from "./media/illustrations/login-logo.svg";
|
||||
import System from "./models/system";
|
||||
|
||||
export const LogoContext = createContext();
|
||||
|
||||
export function LogoProvider({ children }) {
|
||||
const [logo, setLogo] = useState("");
|
||||
const [loginLogo, setLoginLogo] = useState("");
|
||||
const [isCustomLogo, setIsCustomLogo] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchInstanceLogo() {
|
||||
try {
|
||||
const logoURL = await System.fetchLogo();
|
||||
logoURL ? setLogo(logoURL) : setLogo(AnythingLLM);
|
||||
const { isCustomLogo, logoURL } = await System.fetchLogo();
|
||||
if (logoURL) {
|
||||
setLogo(logoURL);
|
||||
setLoginLogo(isCustomLogo ? logoURL : DefaultLoginLogo);
|
||||
setIsCustomLogo(isCustomLogo);
|
||||
} else {
|
||||
setLogo(AnythingLLM);
|
||||
setLoginLogo(DefaultLoginLogo);
|
||||
setIsCustomLogo(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setLogo(AnythingLLM);
|
||||
setLoginLogo(DefaultLoginLogo);
|
||||
setIsCustomLogo(false);
|
||||
console.error("Failed to fetch logo:", err);
|
||||
}
|
||||
}
|
||||
|
||||
fetchInstanceLogo();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LogoContext.Provider value={{ logo, setLogo }}>
|
||||
<LogoContext.Provider value={{ logo, setLogo, loginLogo, isCustomLogo }}>
|
||||
{children}
|
||||
</LogoContext.Provider>
|
||||
);
|
||||
|
@ -168,6 +168,7 @@ export default function MultiUserAuth() {
|
||||
const [token, setToken] = useState(null);
|
||||
const [showRecoveryForm, setShowRecoveryForm] = useState(false);
|
||||
const [showResetPasswordForm, setShowResetPasswordForm] = useState(false);
|
||||
const [customAppName, setCustomAppName] = useState(null);
|
||||
|
||||
const {
|
||||
isOpen: isRecoveryCodeModalOpen,
|
||||
@ -250,6 +251,15 @@ export default function MultiUserAuth() {
|
||||
}
|
||||
}, [downloadComplete, user, token]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomAppName = async () => {
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setLoading(false);
|
||||
};
|
||||
fetchCustomAppName();
|
||||
}, []);
|
||||
|
||||
if (showRecoveryForm) {
|
||||
return (
|
||||
<RecoveryForm
|
||||
@ -272,11 +282,11 @@ export default function MultiUserAuth() {
|
||||
Welcome to
|
||||
</h3>
|
||||
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
|
||||
AnythingLLM
|
||||
{customAppName || "AnythingLLM"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/90 text-center">
|
||||
Sign in to your AnythingLLM account.
|
||||
Sign in to your {customAppName || "AnythingLLM"} account.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import System from "../../../models/system";
|
||||
import { AUTH_TOKEN } from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import paths from "../../../utils/paths";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
@ -10,10 +9,10 @@ import RecoveryCodeModal from "@/components/Modals/DisplayRecoveryCodeModal";
|
||||
export default function SingleUserAuth() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const [recoveryCodes, setRecoveryCodes] = useState([]);
|
||||
const [downloadComplete, setDownloadComplete] = useState(false);
|
||||
const [token, setToken] = useState(null);
|
||||
const [customAppName, setCustomAppName] = useState(null);
|
||||
|
||||
const {
|
||||
isOpen: isRecoveryCodeModalOpen,
|
||||
@ -57,6 +56,15 @@ export default function SingleUserAuth() {
|
||||
}
|
||||
}, [downloadComplete, token]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCustomAppName = async () => {
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setLoading(false);
|
||||
};
|
||||
fetchCustomAppName();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handleLogin}>
|
||||
@ -68,11 +76,11 @@ export default function SingleUserAuth() {
|
||||
Welcome to
|
||||
</h3>
|
||||
<p className="text-4xl md:text-2xl font-bold bg-gradient-to-r from-[#75D6FF] via-[#FFFFFF] to-[#FFFFFF] bg-clip-text text-transparent">
|
||||
AnythingLLM
|
||||
{customAppName || "AnythingLLM"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-white/90 text-center">
|
||||
Sign in to your AnythingLLM instance.
|
||||
Sign in to your {customAppName || "AnythingLLM"} instance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -9,10 +9,9 @@ import {
|
||||
} from "../../../utils/constants";
|
||||
import useLogo from "../../../hooks/useLogo";
|
||||
import illustration from "@/media/illustrations/login-illustration.svg";
|
||||
import loginLogo from "@/media/illustrations/login-logo.svg";
|
||||
|
||||
export default function PasswordModal({ mode = "single" }) {
|
||||
const { logo: _initLogo } = useLogo();
|
||||
const { loginLogo } = useLogo();
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 w-full overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] h-full bg-[#25272C] flex flex-col md:flex-row items-center justify-center">
|
||||
<div
|
||||
@ -37,10 +36,11 @@ export default function PasswordModal({ mode = "single" }) {
|
||||
<div className="flex flex-col items-center justify-center h-full w-full md:w-1/2 z-50 relative">
|
||||
<img
|
||||
src={loginLogo}
|
||||
className={`mb-8 w-[84px] h-[84px] absolute ${
|
||||
mode === "single" ? "md:top-50" : "md:top-36"
|
||||
} top-44 z-30`}
|
||||
alt="logo"
|
||||
alt="Logo"
|
||||
className={`hidden md:flex rounded-2xl w-fit m-4 z-30 ${
|
||||
mode === "single" ? "md:top-[170px]" : "md:top-36"
|
||||
} absolute max-h-[65px] md:bg-login-gradient md:shadow-[0_4px_14px_rgba(0,0,0,0.25)]`}
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
{mode === "single" ? <SingleUserAuth /> : <MultiUserAuth />}
|
||||
</div>
|
||||
|
@ -2,6 +2,6 @@ import { useContext } from "react";
|
||||
import { LogoContext } from "../LogoContext";
|
||||
|
||||
export default function useLogo() {
|
||||
const { logo, setLogo } = useContext(LogoContext);
|
||||
return { logo, setLogo };
|
||||
const { logo, setLogo, loginLogo, isCustomLogo } = useContext(LogoContext);
|
||||
return { logo, setLogo, loginLogo, isCustomLogo };
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ const System = {
|
||||
cacheKeys: {
|
||||
footerIcons: "anythingllm_footer_links",
|
||||
supportEmail: "anythingllm_support_email",
|
||||
customAppName: "anythingllm_custom_app_name",
|
||||
},
|
||||
ping: async function () {
|
||||
return await fetch(`${API_BASE}/ping`)
|
||||
@ -305,19 +306,58 @@ const System = {
|
||||
);
|
||||
return { email: supportEmail, error: null };
|
||||
},
|
||||
|
||||
fetchCustomAppName: async function () {
|
||||
const cache = window.localStorage.getItem(this.cacheKeys.customAppName);
|
||||
const { appName, lastFetched } = cache
|
||||
? safeJsonParse(cache, { appName: "", lastFetched: 0 })
|
||||
: { appName: "", lastFetched: 0 };
|
||||
|
||||
if (!!appName && Date.now() - lastFetched < 3_600_000)
|
||||
return { appName: appName, error: null };
|
||||
|
||||
const { customAppName, error } = await fetch(
|
||||
`${API_BASE}/system/custom-app-name`,
|
||||
{
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
headers: baseHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return { customAppName: "", error: e.message };
|
||||
});
|
||||
|
||||
if (!customAppName || !!error) {
|
||||
window.localStorage.removeItem(this.cacheKeys.customAppName);
|
||||
return { appName: "", error: null };
|
||||
}
|
||||
|
||||
window.localStorage.setItem(
|
||||
this.cacheKeys.customAppName,
|
||||
JSON.stringify({ appName: customAppName, lastFetched: Date.now() })
|
||||
);
|
||||
return { appName: customAppName, error: null };
|
||||
},
|
||||
fetchLogo: async function () {
|
||||
return await fetch(`${API_BASE}/system/logo`, {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ok && res.status !== 204) return res.blob();
|
||||
.then(async (res) => {
|
||||
if (res.ok && res.status !== 204) {
|
||||
const isCustomLogo = res.headers.get("X-Is-Custom-Logo") === "true";
|
||||
const blob = await res.blob();
|
||||
const logoURL = URL.createObjectURL(blob);
|
||||
return { isCustomLogo, logoURL };
|
||||
}
|
||||
throw new Error("Failed to fetch logo!");
|
||||
})
|
||||
.then((blob) => URL.createObjectURL(blob))
|
||||
.catch((e) => {
|
||||
console.log(e);
|
||||
return null;
|
||||
return { isCustomLogo: false, logoURL: null };
|
||||
});
|
||||
},
|
||||
fetchPfp: async function (id) {
|
||||
|
@ -0,0 +1,100 @@
|
||||
import Admin from "@/models/admin";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function CustomAppName() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [customAppName, setCustomAppName] = useState("");
|
||||
const [originalAppName, setOriginalAppName] = useState("");
|
||||
const [canCustomize, setCanCustomize] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInitialParams = async () => {
|
||||
const settings = await System.keys();
|
||||
if (!settings?.MultiUserMode && !settings?.RequiresAuth) {
|
||||
setCanCustomize(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
const { appName } = await System.fetchCustomAppName();
|
||||
setCustomAppName(appName || "");
|
||||
setOriginalAppName(appName || "");
|
||||
setCanCustomize(true);
|
||||
setLoading(false);
|
||||
};
|
||||
fetchInitialParams();
|
||||
}, []);
|
||||
|
||||
const updateCustomAppName = async (e, newValue = null) => {
|
||||
e.preventDefault();
|
||||
let custom_app_name = newValue;
|
||||
if (newValue === null) {
|
||||
const form = new FormData(e.target);
|
||||
custom_app_name = form.get("customAppName");
|
||||
}
|
||||
const { success, error } = await Admin.updateSystemPreferences({
|
||||
custom_app_name,
|
||||
});
|
||||
if (!success) {
|
||||
showToast(`Failed to update custom app name: ${error}`, "error");
|
||||
return;
|
||||
} else {
|
||||
showToast("Successfully updated custom app name.", "success");
|
||||
window.localStorage.removeItem(System.cacheKeys.customAppName);
|
||||
setCustomAppName(custom_app_name);
|
||||
setOriginalAppName(custom_app_name);
|
||||
setHasChanges(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
setCustomAppName(e.target.value);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
if (!canCustomize || loading) return null;
|
||||
|
||||
return (
|
||||
<form className="mb-6" onSubmit={updateCustomAppName}>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h2 className="text-base leading-6 font-bold text-white">
|
||||
Custom App Name
|
||||
</h2>
|
||||
<p className="text-xs leading-[18px] font-base text-white/60">
|
||||
Set a custom app name that is displayed on the login page.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-4">
|
||||
<input
|
||||
name="customAppName"
|
||||
type="text"
|
||||
className="bg-zinc-900 mt-3 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 max-w-[275px] placeholder:text-white/20"
|
||||
placeholder="AnythingLLM"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
onChange={handleChange}
|
||||
value={customAppName}
|
||||
/>
|
||||
{originalAppName !== "" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => updateCustomAppName(e, "")}
|
||||
className="mt-4 text-white text-base font-medium hover:text-opacity-60"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<button
|
||||
type="submit"
|
||||
className="transition-all mt-6 w-fit duration-300 border border-slate-200 px-5 py-2.5 rounded-lg text-white text-sm items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 focus:ring-gray-800"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
@ -2,7 +2,6 @@ import useLogo from "@/hooks/useLogo";
|
||||
import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import AnythingLLM from "@/media/logo/anything-llm.png";
|
||||
import { Plus } from "@phosphor-icons/react";
|
||||
|
||||
export default function CustomLogo() {
|
||||
@ -36,7 +35,7 @@ export default function CustomLogo() {
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
_setLogo(logoURL);
|
||||
|
||||
showToast("Image uploaded successfully.", "success");
|
||||
@ -51,13 +50,13 @@ export default function CustomLogo() {
|
||||
if (!success) {
|
||||
console.error("Failed to remove logo:", error);
|
||||
showToast(`Failed to remove logo: ${error}`, "error");
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
setLogo(logoURL);
|
||||
setIsDefaultLogo(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const logoURL = await System.fetchLogo();
|
||||
const { logoURL } = await System.fetchLogo();
|
||||
_setLogo(logoURL);
|
||||
|
||||
showToast("Image successfully removed.", "success");
|
||||
|
@ -4,6 +4,7 @@ import FooterCustomization from "./FooterCustomization";
|
||||
import SupportEmail from "./SupportEmail";
|
||||
import CustomLogo from "./CustomLogo";
|
||||
import CustomMessages from "./CustomMessages";
|
||||
import CustomAppName from "./CustomAppName";
|
||||
|
||||
export default function Appearance() {
|
||||
return (
|
||||
@ -25,6 +26,7 @@ export default function Appearance() {
|
||||
</p>
|
||||
</div>
|
||||
<CustomLogo />
|
||||
<CustomAppName />
|
||||
<CustomMessages />
|
||||
<FooterCustomization />
|
||||
<SupportEmail />
|
||||
|
@ -355,6 +355,9 @@ function adminEndpoints(app) {
|
||||
?.value,
|
||||
[]
|
||||
) || [],
|
||||
custom_app_name:
|
||||
(await SystemSettings.get({ label: "custom_app_name" }))?.value ||
|
||||
null,
|
||||
};
|
||||
response.status(200).json({ settings });
|
||||
} catch (e) {
|
||||
|
@ -526,17 +526,24 @@ function systemEndpoints(app) {
|
||||
const defaultFilename = getDefaultFilename();
|
||||
const logoPath = await determineLogoFilepath(defaultFilename);
|
||||
const { found, buffer, size, mime } = fetchLogo(logoPath);
|
||||
|
||||
if (!found) {
|
||||
response.sendStatus(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const currentLogoFilename = await SystemSettings.currentLogoFilename();
|
||||
response.writeHead(200, {
|
||||
"Access-Control-Expose-Headers":
|
||||
"Content-Disposition,X-Is-Custom-Logo,Content-Type,Content-Length",
|
||||
"Content-Type": mime || "image/png",
|
||||
"Content-Disposition": `attachment; filename=${path.basename(
|
||||
logoPath
|
||||
)}`,
|
||||
"Content-Length": size,
|
||||
"X-Is-Custom-Logo":
|
||||
currentLogoFilename !== null &&
|
||||
currentLogoFilename !== defaultFilename,
|
||||
});
|
||||
response.end(Buffer.from(buffer, "base64"));
|
||||
return;
|
||||
@ -573,6 +580,22 @@ function systemEndpoints(app) {
|
||||
}
|
||||
});
|
||||
|
||||
// No middleware protection in order to get this on the login page
|
||||
app.get("/system/custom-app-name", async (_, response) => {
|
||||
try {
|
||||
const customAppName =
|
||||
(
|
||||
await SystemSettings.get({
|
||||
label: "custom_app_name",
|
||||
})
|
||||
)?.value ?? null;
|
||||
response.status(200).json({ customAppName: customAppName });
|
||||
} catch (error) {
|
||||
console.error("Error fetching custom app name:", error);
|
||||
response.status(500).json({ message: "Internal server error" });
|
||||
}
|
||||
});
|
||||
|
||||
app.get(
|
||||
"/system/pfp/:id",
|
||||
[validatedRequest, flexUserRoleValid([ROLES.all])],
|
||||
|
@ -27,6 +27,7 @@ const SystemSettings = {
|
||||
"agent_search_provider",
|
||||
"default_agent_skills",
|
||||
"agent_sql_connections",
|
||||
"custom_app_name",
|
||||
],
|
||||
validations: {
|
||||
footer_data: (updates) => {
|
||||
|
Loading…
Reference in New Issue
Block a user