+ {author?.pictureURL && (
+
+ )}
- {authorFullName}
+ {author?.name ?? ""}
- {author.primaryInstitution && (
+ {author?.nameTitle && (
+
{author.nameTitle}
+ )}
+ {author?.nameURL && renderNameURL(author.nameURL)}
+ {author?.companyName && (
- {author.primaryInstitution}
+ {author.companyURL ? (
+
+ {author.companyName}
+
+ ) : (
+ author.companyName
+ )}
)}
- {author.url && (
+ {author?.programName && (
-
-
- {author.url}
-
+
+ {author.programURL ? (
+
+ {author.programName}
+
+ ) : (
+ author.programName
+ )}
)}
- {author.email && (
+ {author?.note && (
-
-
- {author.email}
-
+
+ {author.noteURL ? (
+
+ {author.note}
+
+ ) : (
+ author.note
+ )}
)}
- {author.projects?.length > 0 && (
+ {author?.projects && author.projects.length > 0 && (
- {author.projects.map((p) => (
+ {author.projects.map((p, i) => (
- {`${p.title}${author.projects.length > 1 ? ", " : ""}`}
+ {p.title}{i < author.projects.length - 1 ? ", " : ""}
))}
@@ -230,9 +219,7 @@ const CommonsAuthor = () => {
{
- setItemizedMode(!itemizedMode);
- }}
+ onClick={() => setItemizedMode(!itemizedMode)}
className="bg-slate-100 text-black border border-slate-300 rounded-md !pl-1.5 p-1 shadow-sm hover:shadow-md"
aria-label={
itemizedMode
@@ -257,33 +244,26 @@ const CommonsAuthor = () => {
{itemizedMode ? (
-
+
) : (
)}
-
- {/* Load More Button */}
{hasMore && (
fetchNextPage()}
+ disabled={assetsFetching}
className="px-6 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-wait transition-colors font-semibold"
aria-label="Load more assets"
>
- {isLoading ? "Loading..." : "Load More"}
+ {assetsFetching ? "Loading..." : "Load More"}
)}
-
- {/* End of results message */}
{!hasMore && assets.length > 0 && (
End of results
diff --git a/client/src/screens/commons/File/index.tsx b/client/src/screens/commons/File/index.tsx
index 4618bc64a..13abdff70 100644
--- a/client/src/screens/commons/File/index.tsx
+++ b/client/src/screens/commons/File/index.tsx
@@ -81,7 +81,7 @@ const CommonsFile: React.FC
= () => {
{file?.authors && file?.authors.length > 0 ? (
{file.authors
- .map((a) => `${a.firstName} ${a.lastName}`)
+ .map((a) => a.name)
.join(", ")}
) : (
diff --git a/client/src/screens/commons/Project/index.tsx b/client/src/screens/commons/Project/index.tsx
index 81411d1ec..fb7a0d0bb 100644
--- a/client/src/screens/commons/Project/index.tsx
+++ b/client/src/screens/commons/Project/index.tsx
@@ -98,7 +98,7 @@ const CommonsProject = () => {
{project?.principalInvestigators &&
project?.principalInvestigators.length > 0 ? (
project?.principalInvestigators
- ?.map((p) => `${p.firstName} ${p.lastName}`)
+ ?.map((p) => p.name)
.join(", ")
) : (
No principal investigators
@@ -109,7 +109,7 @@ const CommonsProject = () => {
{project?.coPrincipalInvestigators &&
project?.coPrincipalInvestigators.length > 0 ? (
project?.coPrincipalInvestigators
- ?.map((p) => `${p.firstName} ${p.lastName}`)
+ ?.map((p) => p.name)
.join(", ")
) : (
diff --git a/client/src/screens/conductor/FallbackAuth/index.tsx b/client/src/screens/conductor/FallbackAuth/index.tsx
index 8d7909cd3..94fc7d20e 100644
--- a/client/src/screens/conductor/FallbackAuth/index.tsx
+++ b/client/src/screens/conductor/FallbackAuth/index.tsx
@@ -15,7 +15,7 @@ import Cookies from "js-cookie";
import { isEmptyString } from "../../../components/util/HelperFunctions";
import useGlobalError from "../../../components/error/ErrorHooks";
import styles from './FallbackAuth.module.css';
-import AuthHelper from "../../../components/util/AuthHelper";
+import AuthHelper, { COOKIE_NAMES } from "../../../components/util/AuthHelper";
const FallbackAuth = () => {
const dispatch = useDispatch();
@@ -108,7 +108,7 @@ const FallbackAuth = () => {
if (authRes.data?.err) {
handleGlobalError(authRes.data.errMsg);
}
- if (Cookies.get("conductor_access_v2") !== undefined) {
+ if (Cookies.get(COOKIE_NAMES.ACCESS) !== undefined) {
dispatch({ type: "SET_AUTH" });
if (redirectURI !== "") {
// redirect to the page the user tried to visit directly
diff --git a/client/src/screens/conductor/controlpanel/AuthorsManager/index.tsx b/client/src/screens/conductor/controlpanel/AuthorsManager/index.tsx
new file mode 100644
index 000000000..08358a5ff
--- /dev/null
+++ b/client/src/screens/conductor/controlpanel/AuthorsManager/index.tsx
@@ -0,0 +1,269 @@
+import { useState } from "react";
+import { Link } from "react-router-dom";
+import {
+ Header,
+ Segment,
+ Grid,
+ Breadcrumb,
+ Icon,
+ Dropdown,
+ Input,
+} from "semantic-ui-react";
+import { Author } from "../../../../types";
+import useGlobalError from "../../../../components/error/ErrorHooks";
+import useDebounce from "../../../../hooks/useDebounce";
+import api from "../../../../api";
+import useDocumentTitle from "../../../../hooks/useDocumentTitle";
+import SupportCenterTable from "../../../../components/support/SupportCenterTable";
+import { useInfiniteQuery } from "@tanstack/react-query";
+import { useModals } from "../../../../context/ModalContext";
+import Button from "../../../../components/NextGenComponents/Button";
+import { truncateString } from "../../../../components/util/HelperFunctions";
+import ManageAuthorModal from "../../../../components/controlpanel/AuthorsManager/ManageAuthorModal"
+import GenerateTemplateJSONModal from "../../../../components/controlpanel/AuthorsManager/GenerateTemplateJSONModal"
+
+const LIMIT = 25;
+
+const AuthorsManager = () => {
+ //Global State & Hooks
+ useDocumentTitle("LibreTexts Conductor | Authors Manager");
+ const { handleGlobalError } = useGlobalError();
+ const { debounce } = useDebounce();
+ const { openModal, closeAllModals } = useModals();
+
+ //UI
+ const [sortChoice, setSortChoice] = useState("nameKey");
+ const [searchString, setSearchString] = useState("");
+ const [activeSearch, setActiveSearch] = useState("");
+
+ const sortOptions = [
+ { key: "nameKey", text: "Sort by Name Key", value: "nameKey" },
+ { key: "name", text: "Sort by Name", value: "name" },
+ { key: "companyName", text: "Sort by Company Name", value: "companyName" },
+ ];
+
+ //Data
+ const { data, isFetching, isInitialLoading, fetchNextPage } =
+ useInfiniteQuery({
+ queryKey: ["authors", LIMIT, sortChoice, activeSearch],
+ queryFn: async ({ pageParam = null }) => {
+ const response = await api.getAuthors({
+ limit: LIMIT,
+ page: pageParam || 1,
+ sort: sortChoice,
+ query: activeSearch || undefined,
+ });
+
+ if (response.data.err) {
+ handleGlobalError(
+ response.data.errMsg || "Failed to fetch authors."
+ );
+ return {
+ items: [],
+ meta: { total_count: 0, has_more: false, next_page: null },
+ };
+ }
+ return response.data;
+ },
+ getNextPageParam: (lastPage) => {
+ const nextPage = lastPage?.meta?.has_more && lastPage.meta.next_page ? lastPage.meta.next_page : undefined;
+ const parsed = parseInt(nextPage as string, 10);
+ if (isNaN(parsed)) {
+ return undefined;
+ }
+ return parsed;
+ },
+ staleTime: 1000 * 60 * 5, // 5 minutes
+ refetchOnWindowFocus: false,
+ });
+
+ const allData = data?.pages.flatMap((page) => page.items) || [];
+ const lastPage = data?.pages[data.pages.length - 1];
+
+ function handleOpenManageModal(authorID?: string) {
+ openModal(
+ closeAllModals()} authorID={authorID} />
+ );
+ }
+
+ function renderURLField(url?: string) {
+ if (!url) return null;
+
+ let parsedURL: URL;
+ try {
+ parsedURL = new URL(url);
+ } catch (error) {
+ return null;
+ }
+
+ const truncated = truncateString(parsedURL.toString(), 30);
+
+ return parsedURL.hostname ? (
+
+ {truncated}
+
+ ) : null;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Control Panel
+
+
+ Authors Manager
+
+
+ openModal( closeAllModals()} />)}
+ icon="IconCode"
+ >
+ Generate Template JSON
+
+ handleOpenManageModal()}
+ icon="IconPlus"
+ >
+ Add Author
+
+
+
+
+
+
+
+
+ {
+ setSortChoice(value as string);
+ }}
+ value={sortChoice}
+ />
+
+
+ {
+ setSearchString(e.target.value);
+ debounce((val: string) => setActiveSearch(val), 300)(e.target.value.trim());
+ }}
+ value={searchString}
+ fluid
+ />
+
+
+
+
+
+
+ loading={isInitialLoading}
+ data={allData || []}
+ onRowClick={(author) => {
+ handleOpenManageModal(author._id);
+ }}
+ columns={[
+ {
+ accessor: "nameKey",
+ title: "Name Key (Unique)",
+ copyButton: true,
+ render(record) {
+ return {truncateString(record.nameKey, 30)} ;
+ }
+ },
+ {
+ accessor: "name",
+ title: "Name",
+ render(record, index) {
+ return (
+
+ {record.pictureURL && (
+
+ )}
+
{record.name}
+
+ )
+ },
+ },
+ {
+ accessor: "nameTitle",
+ title: "Name Title",
+ },
+ {
+ accessor: "nameURL",
+ title: "Name URL",
+ render(record) {
+ return renderURLField(record.nameURL);
+ },
+ },
+ {
+ accessor: "companyName",
+ title: "Company Name",
+ },
+ {
+ accessor: "companyURL",
+ title: "Company URL",
+ render(record) {
+ return renderURLField(record.companyURL);
+ },
+ },
+ {
+ accessor: "programName",
+ title: "Program Name",
+ },
+ {
+ accessor: "programURL",
+ title: "Program URL",
+ render(record) {
+ return renderURLField(record.programURL);
+ },
+ },
+ ]}
+ />
+ {lastPage?.meta?.has_more && (
+
+ fetchNextPage()}
+ loading={isFetching || isInitialLoading}
+ variant="primary"
+ icon="IconDownload"
+ >
+ Load More
+
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default AuthorsManager;
diff --git a/client/src/screens/conductor/controlpanel/PeopleManager/index.tsx b/client/src/screens/conductor/controlpanel/PeopleManager/index.tsx
deleted file mode 100644
index 68db18f8b..000000000
--- a/client/src/screens/conductor/controlpanel/PeopleManager/index.tsx
+++ /dev/null
@@ -1,290 +0,0 @@
-import { useState, useEffect, lazy } from "react";
-import { Link } from "react-router-dom";
-import {
- Header,
- Segment,
- Grid,
- Breadcrumb,
- Table,
- Icon,
- Button,
- Dropdown,
- Input,
-} from "semantic-ui-react";
-import { Author } from "../../../../types";
-import useGlobalError from "../../../../components/error/ErrorHooks";
-import { PaginationWithItemsSelect } from "../../../../components/util/PaginationWithItemsSelect";
-import useDebounce from "../../../../hooks/useDebounce";
-import api from "../../../../api";
-const ManagePersonModal = lazy(
- () =>
- import(
- "../../../../components/controlpanel/PeopleManager/ManagePersonModal"
- )
-);
-const BulkAddPeopleModal = lazy(
- () =>
- import(
- "../../../../components/controlpanel/PeopleManager/BulkAddPeopleModal"
- )
-);
-
-const PeopleManager = () => {
- //Global State & Hooks
- const { handleGlobalError } = useGlobalError();
- const { debounce } = useDebounce();
-
- //UI
- const [loading, setLoading] = useState(false);
- const [activePage, setActivePage] = useState(1);
- const [totalItems, setTotalItems] = useState(0);
- const [totalPages, setTotalPages] = useState(1);
- const [itemsPerPage, setItemsPerPage] = useState(10);
- const [sortChoice, setSortChoice] = useState("firstName");
- const [searchString, setSearchString] = useState("");
- const TABLE_COLS = [
- { key: "firstName", text: "First Name" },
- { key: "lastName", text: "Last Name" },
- { key: "email", text: "Email" },
- { key: "primaryInstitution", text: "Primary Institution" },
- { key: "Actions", text: "Actions" },
- ];
-
- const sortOptions = [
- { key: "firstName", text: "Sort by First Name", value: "firstName" },
- { key: "lastName", text: "Sort by Last Name", value: "lastName" },
- { key: "email", text: "Sort by Email", value: "email" },
- ];
-
- //Data
- const [people, setPeople] = useState([]);
- const [selectedPersonId, setSelectedPersonId] = useState(
- undefined
- );
- const [showManageModal, setShowManageModal] = useState(false);
- const [showBulkAddModal, setShowBulkAddModal] = useState(false);
-
- //Effects
- useEffect(() => {
- getPeople(searchString);
- }, [activePage, itemsPerPage, sortChoice]);
-
- // Handlers & Methods
- async function getPeople(searchString?: string) {
- try {
- setLoading(true);
-
- const res = await api.getAuthors({
- page: activePage,
- limit: itemsPerPage,
- query: searchString,
- sort: sortChoice ? sortChoice : undefined,
- });
-
- if (
- res.data.err ||
- !res.data.authors ||
- !Array.isArray(res.data.authors) ||
- res.data.totalCount === undefined
- ) {
- throw new Error("Error retrieving authors");
- }
-
- setPeople(res.data.authors);
- setTotalItems(res.data.totalCount);
- setTotalPages(Math.ceil(res.data.totalCount / itemsPerPage));
- } catch (err) {
- handleGlobalError(err);
- } finally {
- setLoading(false);
- }
- }
-
- const getPeopleDebounced = debounce((searchVal: string) => {
- setActivePage(1); // reset to first page when query changes
- getPeople(searchVal);
- }, 500);
-
- function handleSelectPerson(id?: string) {
- if (!id) return;
- setSelectedPersonId(id);
- setShowManageModal(true);
- }
-
- function handleCloseManageModal() {
- setSelectedPersonId(undefined);
- getPeople(searchString);
- setShowManageModal(false);
- }
-
- function handleCloseBulkAddModal() {
- setSelectedPersonId(undefined);
- setShowBulkAddModal(false);
- getPeople(searchString);
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
- Control Panel
-
-
- People Manager
-
-
- setShowBulkAddModal(true)}
- size="small"
- >
-
- Bulk Add
-
- setShowManageModal(true)}
- size="small"
- className="ml-2"
- >
-
- Add Person
-
-
-
-
-
-
-
-
- {
- setSortChoice(value as string);
- }}
- value={sortChoice}
- />
-
-
- {
- setSearchString(e.target.value);
- getPeopleDebounced(e.target.value.trim());
- }}
- value={searchString}
- fluid
- />
-
-
-
-
-
-
-
-
-
-
-
- {TABLE_COLS.map((item) => (
-
- {item.text}
-
- ))}
-
-
-
- {people.length > 0 &&
- people.map((p) => {
- return (
-
-
- {p.firstName}
-
-
- {p.lastName}
-
-
- {p.email}
-
-
- {p.primaryInstitution}
-
-
- handleSelectPerson(p._id)}
- >
-
- Edit
-
-
-
- );
- })}
- {people.length === 0 && (
-
-
-
- No results found.
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default PeopleManager;
diff --git a/client/src/screens/conductor/controlpanel/StoreManager/index.tsx b/client/src/screens/conductor/controlpanel/StoreManager/index.tsx
index c84384457..1ae463af2 100644
--- a/client/src/screens/conductor/controlpanel/StoreManager/index.tsx
+++ b/client/src/screens/conductor/controlpanel/StoreManager/index.tsx
@@ -1,7 +1,6 @@
import { Link } from "react-router-dom";
import { Header, Segment, Grid, Breadcrumb } from "semantic-ui-react";
import {
- GetStoreOrdersResponse,
StoreOrderWithStripeSession,
} from "../../../../types";
import useGlobalError from "../../../../components/error/ErrorHooks";
@@ -30,7 +29,7 @@ const StoreManager = () => {
const [luluStatusFilter, setLuluStatusFilter] = useState("all");
// const [sortBy, setSortBy] = useState("created");
const { data, isFetching, isInitialLoading, fetchNextPage } =
- useInfiniteQuery({
+ useInfiniteQuery({
queryKey: ["store-orders", limit, statusFilter, luluStatusFilter],
queryFn: async ({ pageParam = null }) => {
const response = await api.adminGetStoreOrders({
@@ -155,13 +154,13 @@ const StoreManager = () => {
{record.createdAt
? new Date(record.createdAt).toLocaleDateString(
- "en-US",
- {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- }
- )
+ "en-US",
+ {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }
+ )
: ""}
);
@@ -183,9 +182,9 @@ const StoreManager = () => {
{record.stripe_session?.amount_total
? formatPrice(
- record.stripe_session.amount_total,
- true
- )
+ record.stripe_session.amount_total,
+ true
+ )
: "$0.00"}
);
@@ -225,14 +224,13 @@ const StoreManager = () => {
render(record) {
return (
{record.luluJobStatus || "--"}
@@ -245,11 +243,10 @@ const StoreManager = () => {
render(record) {
return (
{record.status}
diff --git a/client/src/state/userReducer.ts b/client/src/state/userReducer.ts
index 582100628..e1474b858 100644
--- a/client/src/state/userReducer.ts
+++ b/client/src/state/userReducer.ts
@@ -5,6 +5,7 @@
/* Utils */
import Cookies from "js-cookie";
+import { COOKIE_NAMES } from "../components/util/AuthHelper";
import { User } from "../types";
import { AnyAction } from "redux";
/* User */
@@ -43,7 +44,7 @@ export default function userReducer(
isAuthenticated: true,
};
case "CHECK_AUTH":
- if (Cookies.get("conductor_access_v2") !== undefined) {
+ if (Cookies.get(COOKIE_NAMES.ACCESS) !== undefined) {
return {
...state,
isAuthenticated: true,
diff --git a/client/src/types/Author.ts b/client/src/types/Author.ts
index 26947107b..0a18251f9 100644
--- a/client/src/types/Author.ts
+++ b/client/src/types/Author.ts
@@ -1,10 +1,19 @@
export type Author = {
_id?: string;
- firstName: string;
- lastName: string;
- email?: string;
- url?: string;
- primaryInstitution?: string;
+ orgID: string;
+ nameKey: string; // Unique identifier for the author within an org
+ name: string;
+ nameTitle?: string;
+ nameURL?: string;
+ note?: string;
+ noteURL?: string;
+ companyName?: string;
+ companyURL?: string;
+ pictureCircle?: string; // i.e. "yes" or "no"
+ pictureURL?: string;
+ programName?: string;
+ programURL?: string;
+ attributionPrefix?: string;
userUUID?: string;
-}
\ No newline at end of file
+}
diff --git a/client/src/types/Misc.ts b/client/src/types/Misc.ts
index 7250f4fcc..33a637398 100644
--- a/client/src/types/Misc.ts
+++ b/client/src/types/Misc.ts
@@ -24,6 +24,19 @@ export type ConductorBaseResponse =
| { err: false }
| { err: true; errMsg: string };
+export type ConductorInfiniteScrollResponse = {
+ err: false;
+ items: T[];
+ meta: {
+ total_count: number;
+ has_more: boolean;
+ next_page: string | number | null;
+ }
+} | {
+ err: true;
+ errMsg: string;
+};
+
export type _MoveFile = Pick<
ProjectFile,
"fileID" | "name" | "storageType" | "description"
diff --git a/client/src/types/Store.ts b/client/src/types/Store.ts
index 95b63629c..5cf951c11 100644
--- a/client/src/types/Store.ts
+++ b/client/src/types/Store.ts
@@ -99,12 +99,3 @@ export type StoreOrderWithStripeSession = StoreOrder & {
stripe_session: Stripe.Checkout.Session;
stripe_charge?: Stripe.Charge | null;
}
-
-export type GetStoreOrdersResponse = {
- items: StoreOrderWithStripeSession[];
- meta: {
- total_count: number;
- has_more: boolean;
- next_page: string | null;
- };
-}
\ No newline at end of file
diff --git a/client/src/utils/assetHelpers.ts b/client/src/utils/assetHelpers.ts
index ba6e1265c..443ef0598 100644
--- a/client/src/utils/assetHelpers.ts
+++ b/client/src/utils/assetHelpers.ts
@@ -113,14 +113,14 @@ export function getPrettyAuthorsList(
const authorList =
authors
- .filter((a) => !!a.firstName && !!a.lastName)
- .map((a) => `${a.firstName} ${a.lastName}`)
+ .filter((a) => a?.name)
+ .map((a) => a.name)
.join(", ") || "Unknown";
return primaryAuthor
- ? `${primaryAuthor.firstName} ${primaryAuthor.lastName} et al.`
+ ? `${primaryAuthor.name} et al.`
: correspondingAuthor
- ? `${correspondingAuthor.firstName} ${correspondingAuthor.lastName}, ${authorList}`
+ ? `${correspondingAuthor.name}, ${authorList}`
: authorList;
}
diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts
index 11f02fe2a..2d8d36ceb 100644
--- a/client/src/vite-env.d.ts
+++ b/client/src/vite-env.d.ts
@@ -1 +1,5 @@
///
+
+interface Window {
+ __APP_ENV__: string; // This will be injected at runtime by the server
+}
diff --git a/server/api.js b/server/api.js
index 40a4d5bdb..850bcea9b 100644
--- a/server/api.js
+++ b/server/api.js
@@ -742,27 +742,17 @@ router
router
.route("/authors")
.get(
- middleware.validateZod(AuthorsValidators.GetAllAuthorsValidator),
+ middleware.validateZod(AuthorsValidators.GetAuthorsValidator),
authorsAPI.getAuthors
)
.post(
authAPI.verifyRequest,
authAPI.getUserAttributes,
- authAPI.checkHasRoleMiddleware(process.env.ORG_ID, "campusadmin"),
+ authAPI.checkHasRoleMiddleware("libretexts", "superadmin"),
middleware.validateZod(AuthorsValidators.CreateAuthorValidator),
authorsAPI.createAuthor
);
-router
- .route("/authors/bulk")
- .post(
- authAPI.verifyRequest,
- authAPI.getUserAttributes,
- authAPI.checkHasRoleMiddleware(process.env.ORG_ID, "campusadmin"),
- middleware.validateZod(AuthorsValidators.BulkCreateAuthorsValidator),
- authorsAPI.bulkCreateAuthors
- );
-
router
.route("/authors/:id")
.get(
@@ -772,14 +762,14 @@ router
.patch(
authAPI.verifyRequest,
authAPI.getUserAttributes,
- authAPI.checkHasRoleMiddleware(process.env.ORG_ID, "campusadmin"),
+ authAPI.checkHasRoleMiddleware("libretexts", "superadmin"),
middleware.validateZod(AuthorsValidators.UpdateAuthorValidator),
authorsAPI.updateAuthor
)
.delete(
authAPI.verifyRequest,
authAPI.getUserAttributes,
- authAPI.checkHasRoleMiddleware(process.env.ORG_ID, "campusadmin"),
+ authAPI.checkHasRoleMiddleware("libretexts", "superadmin"),
middleware.validateZod(AuthorsValidators.DeleteAuthorValidator),
authorsAPI.deleteAuthor
);
diff --git a/server/api/auth.ts b/server/api/auth.ts
index 1003ceada..038771efd 100644
--- a/server/api/auth.ts
+++ b/server/api/auth.ts
@@ -21,6 +21,16 @@ const SALT_ROUNDS = 10;
const JWT_SECRET = new TextEncoder().encode(process.env.SECRETKEY);
const JWT_COOKIE_DOMAIN = (process.env.PRODUCTIONURLS || "").split(",")[0];
const SESSION_DEFAULT_EXPIRY_MINUTES = 60 * 24 * 7; // 7 days
+
+const APP_ENV = process.env.APP_ENV ?? "production";
+const COOKIE_PREFIX = APP_ENV === "production" ? "conductor" : `conductor_${APP_ENV}`;
+export const COOKIE_NAMES = {
+ ACCESS: `${COOKIE_PREFIX}_access_v2`,
+ SIGNED: `${COOKIE_PREFIX}_signed_v2`,
+ OIDC_STATE: `${COOKIE_PREFIX}_oidc_state`,
+ OIDC_NONCE: `${COOKIE_PREFIX}_oidc_nonce`,
+ AUTH_REDIRECT: `${COOKIE_PREFIX}_auth_redirect`,
+};
const SESSION_DEFAULT_EXPIRY_MILLISECONDS =
SESSION_DEFAULT_EXPIRY_MINUTES * 60 * 1000;
@@ -106,11 +116,11 @@ async function createAndAttachLocalSession(
domain: JWT_COOKIE_DOMAIN,
maxAge: SESSION_DEFAULT_EXPIRY_MILLISECONDS,
};
- res.cookie("conductor_access_v2", access, {
+ res.cookie(COOKIE_NAMES.ACCESS, access, {
path: "/",
...(process.env.NODE_ENV === "production" && prodCookieConfig),
});
- res.cookie("conductor_signed_v2", signed, {
+ res.cookie(COOKIE_NAMES.SIGNED, signed, {
path: "/",
httpOnly: true,
...(process.env.NODE_ENV === "production" && prodCookieConfig),
@@ -169,19 +179,19 @@ async function initLogin(req: Request, res: Response) {
process.env.CONDUCTOR_DOMAIN !== "commons.libretexts.org"
) {
const authRedirectURL = `${oidcCallbackProto}://${process.env.CONDUCTOR_DOMAIN}`;
- res.cookie("conductor_auth_redirect", authRedirectURL, {
+ res.cookie(COOKIE_NAMES.AUTH_REDIRECT, authRedirectURL, {
encode: String,
httpOnly: true,
...(process.env.NODE_ENV === "production" && prodCookieConfig),
});
}
- res.cookie("oidc_state", base64State, {
+ res.cookie(COOKIE_NAMES.OIDC_STATE, base64State, {
encode: String,
httpOnly: true,
...(process.env.NODE_ENV === "production" && prodCookieConfig),
});
- res.cookie("oidc_nonce", nonce, {
+ res.cookie(COOKIE_NAMES.OIDC_NONCE, nonce, {
encode: String,
httpOnly: true,
...(process.env.NODE_ENV === "production" && prodCookieConfig),
@@ -208,7 +218,7 @@ async function completeLogin(req: Request, res: Response) {
encodeURIComponent(value).replace(/%20/g, "+");
// Compare state nonce
- const { oidc_state } = req.cookies;
+ const oidc_state = req.cookies[COOKIE_NAMES.OIDC_STATE];
const { state: stateQuery } = req.query;
const safeParseState = (stateStr: string) => {
@@ -262,7 +272,7 @@ async function completeLogin(req: Request, res: Response) {
});
// Compare nonce hash
- const { oidc_nonce } = req.cookies;
+ const oidc_nonce = req.cookies[COOKIE_NAMES.OIDC_NONCE];
const { nonce } = payload;
const nonceString = nonce?.toString();
if (!nonce || !oidc_nonce || !nonceString) {
@@ -370,8 +380,8 @@ async function completeLogin(req: Request, res: Response) {
// Determine base of redirect URL
let finalRedirectURL = `${oidcCallbackProto}://${oidcCallbackHost}`; // Default to callback host
- if(req.cookies.conductor_auth_redirect){
- finalRedirectURL = req.cookies.conductor_auth_redirect; // Use auth redirect cookie if available
+ if(req.cookies[COOKIE_NAMES.AUTH_REDIRECT]){
+ finalRedirectURL = req.cookies[COOKIE_NAMES.AUTH_REDIRECT]; // Use auth redirect cookie if available
}
// Check if redirectURI is a full URL and decode if needed
@@ -414,8 +424,8 @@ async function completeLogin(req: Request, res: Response) {
async function logout(_req: Request, res: Response) {
try {
// Attempt to invalidate the user's session
- const accessCookie = _req.cookies.conductor_access_v2;
- const signedCookie = _req.cookies.conductor_signed_v2;
+ const accessCookie = _req.cookies[COOKIE_NAMES.ACCESS];
+ const signedCookie = _req.cookies[COOKIE_NAMES.SIGNED];
const sessionJWT = `${accessCookie}.${signedCookie}`;
if (accessCookie && signedCookie && sessionJWT) {
try {
@@ -445,11 +455,11 @@ async function logout(_req: Request, res: Response) {
secure: true,
domain: JWT_COOKIE_DOMAIN,
};
- res.clearCookie("conductor_access_v2", {
+ res.clearCookie(COOKIE_NAMES.ACCESS, {
path: "/",
...(process.env.NODE_ENV === "production" && prodCookieConfig),
});
- res.clearCookie("conductor_signed_v2", {
+ res.clearCookie(COOKIE_NAMES.SIGNED, {
path: "/",
httpOnly: true,
...(process.env.NODE_ENV === "production" && prodCookieConfig),
diff --git a/server/api/authors.ts b/server/api/authors.ts
index abdcf133b..20891d023 100644
--- a/server/api/authors.ts
+++ b/server/api/authors.ts
@@ -2,63 +2,30 @@ import { z } from "zod";
import { debugError } from "../debug.js";
import Author from "../models/author.js";
import {
- BulkCreateAuthorsValidator,
CreateAuthorValidator,
DeleteAuthorValidator,
- GetAllAuthorsValidator,
+ GetAuthorsValidator,
GetAuthorAssetsValidator,
GetAuthorValidator,
UpdateAuthorValidator,
} from "./validators/authors.js";
import { Response } from "express";
-import { conductor500Err } from "../util/errorutils.js";
+import { conductor404Err, conductor500Err } from "../util/errorutils.js";
import { getPaginationOffset } from "../util/helpers.js";
import ProjectFile from "../models/projectfile.js";
-import { Types } from "mongoose";
+import AuthorService from "./services/author-service.js";
async function getAuthors(
- req: z.infer,
+ req: z.infer,
res: Response
) {
try {
- const limit = req.query.limit || 10;
- const offset = getPaginationOffset(req.query.page, limit);
-
- const queryObj: Record = {
- $and: [{ orgID: process.env.ORG_ID }],
- };
-
- if (req.query.query) {
- queryObj.$and.push({
- $or: [
- { firstName: { $regex: req.query.query, $options: "i" } },
- { lastName: { $regex: req.query.query, $options: "i" } },
- { email: { $regex: req.query.query, $options: "i" } },
- ],
- });
- }
-
- const authorsPromise = Author.find(queryObj)
- .skip(offset)
- .limit(limit)
- .sort(req.query.sort || "lastName")
- .lean();
-
- const totalPromise = Author.countDocuments(queryObj);
-
- const [authors, total] = await Promise.allSettled([
- authorsPromise,
- totalPromise,
- ]);
-
- if (authors.status === "rejected" || total.status === "rejected") {
- return conductor500Err(res);
- }
+ const authorService = new AuthorService();
+ const data = await authorService.getAuthors(req.query);
res.send({
err: false,
- authors: authors.value,
- totalCount: total.value,
+ ...data,
});
} catch (error) {
debugError(error);
@@ -71,18 +38,8 @@ async function getAuthor(
res: Response
) {
try {
- const convertedID = new Types.ObjectId(req.params.id);
- const aggRes = await Author.aggregate([
- {
- $match: {
- _id: convertedID,
- orgID: process.env.ORG_ID,
- },
- },
- LOOKUP_AUTHOR_PROJECTS,
- ]);
-
- const author = aggRes[0];
+ const authorService = new AuthorService();
+ const author = await authorService.getAuthorByID(req.params.id);
if (!author) {
return res.status(404).send({
@@ -96,12 +53,6 @@ async function getAuthor(
author,
});
} catch (err: any) {
- if (err.name === "DocumentNotFoundError") {
- return res.status(404).send({
- err: true,
- message: "Author not found",
- });
- }
debugError(err);
return conductor500Err(res);
}
@@ -267,10 +218,7 @@ async function getAuthorAssets(
});
} catch (err: any) {
if (err.name === "DocumentNotFoundError") {
- return res.status(404).send({
- err: true,
- message: "Author not found",
- });
+ return conductor404Err(res);
}
debugError(err);
return conductor500Err(res);
@@ -282,52 +230,8 @@ async function createAuthor(
res: Response
) {
try {
- const {
- firstName,
- lastName,
- email,
- primaryInstitution,
- url,
- isAdminEntry,
- } = req.body;
-
- // If an author was already created with the same email (and was not admin entry), we should "merge" the two, preferring the admin's data
- if (email) {
- const existingAuthor = await Author.findOne({
- email,
- isAdminEntry: false,
- orgID: process.env.ORG_ID,
- });
-
- if (existingAuthor) {
- await Author.updateOne(
- { _id: existingAuthor._id },
- {
- $set: {
- isAdminEntry: true,
- firstName,
- lastName,
- primaryInstitution,
- ...(url && { url }),
- },
- }
- );
- return res.send({
- err: false,
- author: existingAuthor,
- });
- }
- }
-
- const author = await Author.create({
- firstName,
- lastName,
- primaryInstitution,
- ...(email && { email }),
- ...(url && { url }),
- isAdminEntry: true, // only Campus Admins use this endpoint so set to true
- orgID: process.env.ORG_ID,
- });
+ const authorService = new AuthorService();
+ const author = await authorService.createAuthor(req.body);
res.send({
err: false,
@@ -337,45 +241,7 @@ async function createAuthor(
if (err.name === "MongoServerError" && err.code === 11000) {
return res.status(409).send({
err: true,
- message: "Author with that email already exists",
- });
- }
- debugError(err);
- return conductor500Err(res);
- }
-}
-
-async function bulkCreateAuthors(
- req: z.infer,
- res: Response
-) {
- try {
- const existingAuthorEmails = (await Author.find().lean()).map(
- (author) => author.email
- );
-
- // Ignore duplicates (email)
- const noDuplicates = req.body.authors.filter(
- (author) => !existingAuthorEmails.includes(author.email)
- );
-
- const withAdminFlag = noDuplicates.map((author) => ({
- ...author,
- isAdminEntry: true,
- orgID: process.env.ORG_ID,
- }));
-
- const insertRes = await Author.insertMany(withAdminFlag);
-
- return res.send({
- err: false,
- authors: insertRes,
- });
- } catch (err: any) {
- if (err.name === "MongoServerError" && err.code === 11000) {
- return res.status(409).send({
- err: true,
- message: "Author with that email already exists",
+ errMsg: "An author with that nameKey already exists.",
});
}
debugError(err);
@@ -388,37 +254,22 @@ async function updateAuthor(
res: Response
) {
try {
- const { firstName, lastName, email, primaryInstitution, url } = req.body;
- await Author.updateOne(
- { _id: req.params.id, orgID: process.env.ORG_ID },
- {
- firstName,
- lastName,
- primaryInstitution,
- ...(email && { email }),
- ...(url && { url }),
- orgID: process.env.ORG_ID,
- }
- ).orFail();
-
- const updated = await Author.findById(req.params.id).orFail().lean();
+ const authorService = new AuthorService();
+ const author = await authorService.updateAuthor(req.params.id, req.body);
return res.send({
err: false,
- author: updated,
+ author,
});
} catch (err: any) {
if (err.name === "MongoServerError" && err.code === 11000) {
return res.status(409).send({
err: true,
- message: "Author with that email already exists",
+ errMsg: "An author with that nameKey already exists.",
});
}
if (err.name === "DocumentNotFoundError") {
- return res.status(404).send({
- err: true,
- message: "Author not found",
- });
+ return conductor404Err(res);
}
debugError(err);
return conductor500Err(res);
@@ -430,10 +281,8 @@ async function deleteAuthor(
res: Response
) {
try {
- await Author.deleteOne({
- _id: req.params.id,
- orgID: process.env.ORG_ID,
- }).orFail();
+ const authorService = new AuthorService();
+ await authorService.deleteAuthor(req.params.id);
return res.send({
err: false,
@@ -441,104 +290,20 @@ async function deleteAuthor(
});
} catch (err: any) {
if (err.name === "DocumentNotFoundError") {
- return res.status(404).send({
- err: true,
- message: "Author not found",
- });
+ return conductor404Err(res);
}
debugError(err);
return conductor500Err(res);
}
}
-const LOOKUP_AUTHOR_PROJECTS = {
- $lookup: {
- from: "projects",
- let: {
- authorID: "$_id",
- },
- pipeline: [
- {
- $match: {
- orgID: process.env.ORG_ID,
- visibility: "public",
- },
- },
- {
- $match: {
- $expr: {
- $or: [
- {
- $eq: ["$defaultPrimaryAuthorID", "$$authorID"],
- },
- {
- $eq: ["$defaultCorrespondingAuthorID", "$$authorID"],
- },
- {
- $in: [
- "$$authorID",
- {
- $cond: {
- if: {
- $isArray: "$defaultSecondaryAuthorIDs",
- },
- then: "$defaultSecondaryAuthorIDs",
- else: [],
- },
- },
- ],
- },
- {
- $in: [
- "$$authorID",
- {
- $cond: {
- if: {
- $isArray: "$principalInvestigatorIDs",
- },
- then: "$principalInvestigatorIDs",
- else: [],
- },
- },
- ],
- },
- {
- $in: [
- "$$authorID",
- {
- $cond: {
- if: {
- $isArray: "$coPrincipalInvestigatorIDs",
- },
- then: "$coPrincipalInvestigatorIDs",
- else: [],
- },
- },
- ],
- },
- ],
- },
- },
- },
- {
- $project: {
- _id: 0,
- projectID: 1,
- title: 1,
- },
- },
- ],
- as: "projects",
- },
-};
+
export default {
getAuthors,
getAuthor,
getAuthorAssets,
createAuthor,
- bulkCreateAuthors,
updateAuthor,
deleteAuthor,
- LOOKUP_AUTHOR_PROJECTS,
};
diff --git a/server/api/search.ts b/server/api/search.ts
index 1da017d8a..bc565ec7e 100644
--- a/server/api/search.ts
+++ b/server/api/search.ts
@@ -37,6 +37,7 @@ import { _getBookPublicOrInstructorAssetsCount, buildOrganizationNamesList } fro
import CustomCatalog, { CustomCatalogInterface } from "../models/customcatalog.js";
import { normalizedSort } from "../util/searchutils.js";
import SearchService from "./services/search-service.js";
+import AuthorService from "./services/author-service.js";
const searchQueryCache: SearchQueryInterface_Raw[] = []; // in-memory cache for search queries
@@ -1890,7 +1891,7 @@ async function authorsSearch(
orgID: process.env.ORG_ID,
},
},
- authorsAPI.LOOKUP_AUTHOR_PROJECTS,
+ AuthorService.LOOKUP_AUTHOR_PROJECTS_STAGE,
{
$project: {
_id: 1,
diff --git a/server/api/services/author-service.ts b/server/api/services/author-service.ts
new file mode 100644
index 000000000..b3856421c
--- /dev/null
+++ b/server/api/services/author-service.ts
@@ -0,0 +1,183 @@
+import { z } from "zod";
+import { GetAuthorsValidator, CreateAuthorValidator, UpdateAuthorValidator } from "../validators/authors.js";
+import { BaseConductorInfiniteScrollResponse } from "../../types";
+import Author, { AuthorInterface } from "../../models/author.js";
+import { escapeRegEx, getPaginationOffset } from "../../util/helpers.js";
+import { Types } from "mongoose";
+
+export default class AuthorService {
+ public async getAuthors(params: z.infer['query']): Promise> {
+ let filter: any = { orgID: process.env.ORG_ID };
+
+ if (params.query) {
+ const queryRegex = new RegExp(escapeRegEx(params.query), "i");
+ filter.$or = [{ name: queryRegex }, { nameKey: queryRegex }, { companyName: queryRegex }, { programName: queryRegex }];
+ }
+
+ const offset = getPaginationOffset(params.page, params.limit);
+
+ const authors = await Author.find(filter).sort({ [params.sort]: 1 }).skip(offset).limit(params.limit).lean();
+ const total = await Author.countDocuments(filter);
+
+ const has_more = offset + params.limit < total;
+ const next_page = has_more ? params.page + 1 : null;
+
+ return {
+ items: authors as unknown as AuthorInterface[],
+ meta: {
+ has_more,
+ next_page,
+ total_count: total,
+ }
+ };
+ }
+
+ public async getAuthorByID(id: string): Promise {
+ const convertedID = new Types.ObjectId(id);
+
+ const aggRes = await Author.aggregate([
+ {
+ $match: {
+ _id: convertedID,
+ orgID: process.env.ORG_ID,
+ },
+ },
+ AuthorService.LOOKUP_AUTHOR_PROJECTS_STAGE,
+ ]);
+
+ return aggRes.length > 0 ? aggRes[0] : null;
+ }
+
+ public async createAuthor(data: z.infer['body']): Promise {
+ const author = await Author.create({
+ ...this.sanitizeAuthorData(data),
+ orgID: process.env.ORG_ID,
+ });
+ return author;
+ }
+
+ public async updateAuthor(id: string, data: z.infer['body']): Promise {
+ const toSet: Record = {};
+ const toUnset: Record = {};
+
+ for (const [key, value] of Object.entries(this.sanitizeAuthorData(data))) {
+ if (value !== undefined) {
+ toSet[key] = value;
+ }
+ }
+
+ // Unset fields that were explicitly cleared (empty string sent by client)
+ for (const [key, value] of Object.entries(data)) {
+ if (key !== 'pictureCircle' && value === "") {
+ toUnset[key] = 1;
+ }
+ }
+
+ await Author.updateOne(
+ { _id: id, orgID: process.env.ORG_ID },
+ {
+ ...(Object.keys(toSet).length > 0 && { $set: toSet }),
+ ...(Object.keys(toUnset).length > 0 && { $unset: toUnset }),
+ }
+ ).orFail();
+
+ return await Author.findById(id).orFail().lean() as unknown as AuthorInterface;
+ }
+
+ public async deleteAuthor(id: string): Promise {
+ await Author.deleteOne({ _id: id, orgID: process.env.ORG_ID }).orFail();
+ }
+
+ private sanitizeAuthorData(data: Partial['body']>): Record {
+ const result: Record = {};
+
+ for (const [key, value] of Object.entries(data)) {
+ if (value !== "" && value !== undefined) {
+ result[key] = value;
+ }
+ }
+
+ return result;
+ }
+
+ static readonly LOOKUP_AUTHOR_PROJECTS_STAGE = {
+ $lookup: {
+ from: "projects",
+ let: {
+ authorID: "$_id",
+ },
+ pipeline: [
+ {
+ $match: {
+ orgID: process.env.ORG_ID,
+ visibility: "public",
+ },
+ },
+ {
+ $match: {
+ $expr: {
+ $or: [
+ {
+ $eq: ["$defaultPrimaryAuthorID", "$$authorID"],
+ },
+ {
+ $eq: ["$defaultCorrespondingAuthorID", "$$authorID"],
+ },
+ {
+ $in: [
+ "$$authorID",
+ {
+ $cond: {
+ if: {
+ $isArray: "$defaultSecondaryAuthorIDs",
+ },
+ then: "$defaultSecondaryAuthorIDs",
+ else: [],
+ },
+ },
+ ],
+ },
+ {
+ $in: [
+ "$$authorID",
+ {
+ $cond: {
+ if: {
+ $isArray: "$principalInvestigatorIDs",
+ },
+ then: "$principalInvestigatorIDs",
+ else: [],
+ },
+ },
+ ],
+ },
+ {
+ $in: [
+ "$$authorID",
+ {
+ $cond: {
+ if: {
+ $isArray: "$coPrincipalInvestigatorIDs",
+ },
+ then: "$coPrincipalInvestigatorIDs",
+ else: [],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ {
+ $project: {
+ _id: 0,
+ projectID: 1,
+ title: 1,
+ },
+ },
+ ],
+ as: "projects",
+ },
+ };
+}
diff --git a/server/api/validators/authors.ts b/server/api/validators/authors.ts
index 883d5051d..b3ad02eb0 100644
--- a/server/api/validators/authors.ts
+++ b/server/api/validators/authors.ts
@@ -2,12 +2,20 @@ import { z } from "zod";
import { PaginationSchema, isMongoIDValidator } from "./misc.js";
const _AuthorValidator = z.object({
- firstName: z.string().trim().min(1).max(100),
- lastName: z.string().trim().min(1).max(100),
- email: z.string().trim().email().optional().or(z.literal("")),
- url: z.string().url().optional().or(z.literal("")),
- primaryInstitution: z.string().trim().optional().or(z.literal("")),
- userUUID: z.string().uuid().optional(),
+ nameKey: z.string().trim().min(1).max(100),
+ name: z.string().trim().min(1).max(200),
+ nameTitle: z.string().trim().max(50).optional().or(z.literal("")),
+ nameURL: z.url().optional().or(z.literal("")),
+ note: z.string().trim().max(1000).optional().or(z.literal("")),
+ noteURL: z.url().optional().or(z.literal("")),
+ companyName: z.string().trim().max(200).optional().or(z.literal("")),
+ companyURL: z.url().optional().or(z.literal("")),
+ pictureCircle: z.enum(["yes", "no"]).optional(),
+ pictureURL: z.url().optional().or(z.literal("")),
+ programName: z.string().trim().max(200).optional().or(z.literal("")),
+ programURL: z.url().optional().or(z.literal("")),
+ attributionPrefix: z.string().trim().max(100).optional().or(z.literal("")),
+ userUUID: z.uuid().optional(),
});
const AuthorIDParams = z.object({
@@ -16,13 +24,14 @@ const AuthorIDParams = z.object({
}),
});
-export const GetAllAuthorsValidator = z.object({
+export const GetAuthorsValidator = z.object({
query: z
.object({
- query: z.string().optional(),
- sort: z.enum(["firstName", "lastName", "email"]).optional(),
- })
- .merge(PaginationSchema),
+ page: z.coerce.number().min(1).optional().default(1),
+ limit: z.coerce.number().int().min(1).optional().default(25),
+ query: z.string().optional().or(z.literal("")),
+ sort: z.enum(["nameKey", "name", "companyName"]).optional().default("nameKey"),
+ }).optional().default({ page: 1, limit: 25, sort: "nameKey" }),
});
export const GetAuthorValidator = AuthorIDParams;
@@ -34,20 +43,12 @@ export const GetAuthorAssetsValidator = z.object({
});
export const CreateAuthorValidator = z.object({
- body: _AuthorValidator.merge(
- z.object({
- isAdminEntry: z.boolean().optional().default(false),
- })
- ),
+ body: _AuthorValidator,
});
-export const UpdateAuthorValidator =
- CreateAuthorValidator.merge(AuthorIDParams);
+export const UpdateAuthorValidator = z.object({
+ body: _AuthorValidator.partial(),
+ params: AuthorIDParams.shape.params,
+});
export const DeleteAuthorValidator = AuthorIDParams;
-
-export const BulkCreateAuthorsValidator = z.object({
- body: z.object({
- authors: z.array(_AuthorValidator).min(1).max(1500),
- }),
-});
diff --git a/server/middleware.ts b/server/middleware.ts
index a544cde60..b8c661b2f 100644
--- a/server/middleware.ts
+++ b/server/middleware.ts
@@ -9,7 +9,7 @@ import { validationResult } from "express-validator";
import conductorErrors from "./conductor-errors.js";
import { ZodObject } from "zod";
import { debugError } from "./debug.js";
-import authAPI from "./api/auth.js";
+import authAPI, { COOKIE_NAMES } from "./api/auth.js";
import {
TypedReqBodyWithUser,
TypedReqParamsAndBodyWithUser,
@@ -118,10 +118,10 @@ function authSanitizer(req: Request, _res: Response, next: NextFunction) {
const { cookies } = req;
if (
!req.header("authorization") &&
- cookies.conductor_access_v2 &&
- cookies.conductor_signed_v2
+ cookies[COOKIE_NAMES.ACCESS] &&
+ cookies[COOKIE_NAMES.SIGNED]
) {
- req.headers.authorization = `${cookies.conductor_access_v2}.${cookies.conductor_signed_v2}`;
+ req.headers.authorization = `${cookies[COOKIE_NAMES.ACCESS]}.${cookies[COOKIE_NAMES.SIGNED]}`;
}
}
return next();
diff --git a/server/migrations/ImportAuthorsFromJSON.js b/server/migrations/ImportAuthorsFromJSON.js
new file mode 100644
index 000000000..def683aca
--- /dev/null
+++ b/server/migrations/ImportAuthorsFromJSON.js
@@ -0,0 +1,275 @@
+import fs from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import mongoose from "mongoose";
+import Author from "../models/author.js";
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+/**
+ * Imports author records from prod-authors-header.json and prod-authors-footer.json
+ * into the Author collection.
+ *
+ * Merge rules:
+ * - Within each file, the FIRST occurrence of a duplicate key wins.
+ * - Between files, prod-authors-header.json takes precedence over prod-authors-footer.json.
+ * - Records already present in the DB (matched by nameKey + orgID) are skipped (idempotent).
+ *
+ * Both files contain invalid JSON escape sequences (\') which are sanitized before parsing.
+ *
+ * Field mapping (source keys are all lowercase in the JSON):
+ * root key → nameKey
+ * name → name
+ * nametitle → nameTitle
+ * nameurl → nameURL
+ * note → note
+ * noteurl → noteURL
+ * companyname → companyName
+ * companyurl → companyURL
+ * picturecircle → pictureCircle ("yes" / "no" string)
+ * pictureurl → pictureURL
+ * programname → programName
+ * programurl → programURL
+ * attributionprefix → attributionPrefix
+ */
+
+const FIELD_MAP = {
+ name: "name",
+ nametitle: "nameTitle",
+ nameurl: "nameURL",
+ note: "note",
+ noteurl: "noteURL",
+ companyname: "companyName",
+ companyurl: "companyURL",
+ picturecircle: "pictureCircle",
+ pictureurl: "pictureURL",
+ programname: "programName",
+ programurl: "programURL",
+ attributionprefix: "attributionPrefix",
+};
+
+/**
+ * Replace invalid JSON escape sequences involving apostrophes with a plain apostrophe.
+ * Both \' and \\' appear in the source files (single and double-escaped apostrophes),
+ * so we collapse any run of one or more backslashes immediately before a ' into
+ * just the apostrophe itself.
+ * Standard JSON only allows \", \\, \/, \b, \f, \n, \r, \t, \uXXXX.
+ */
+function fixEscapes(raw) {
+ return raw.replace(/\\+'(?!')/g, "'");
+}
+
+/**
+ * Parse a JSON file, preserving first-occurrence order for duplicate top-level keys.
+ *
+ * JSON.parse gives last-wins for duplicate keys (undefined behavior per spec, but
+ * consistent in V8). Since both source files contain many within-file duplicates, we
+ * scan the raw text for top-level key positions and extract each object value via
+ * brace-matching, skipping any key we have already seen.
+ */
+function parseFileFirstWins(filePath) {
+ const raw = fs.readFileSync(filePath, "utf8");
+ const fixed = fixEscapes(raw);
+
+ const result = {};
+ const seenKeys = new Set();
+
+ // Match top-level keys: a line that starts with optional whitespace,
+ // a quoted string, a colon, and an opening brace.
+ const keyPattern = /\n[ \t]*"([^"]+)"[ \t]*:[ \t]*\{/g;
+ let match;
+
+ while ((match = keyPattern.exec(fixed)) !== null) {
+ const key = match[1];
+ if (seenKeys.has(key)) continue;
+ seenKeys.add(key);
+
+ // Walk forward from the opening brace of this value using brace-matching.
+ const openBrace = match.index + match[0].length - 1; // index of '{'
+ let depth = 0;
+ let i = openBrace;
+ let inString = false;
+ let prevBackslash = false;
+
+ while (i < fixed.length) {
+ const ch = fixed[i];
+
+ if (prevBackslash) {
+ prevBackslash = false;
+ i++;
+ continue;
+ }
+
+ if (ch === "\\" && inString) {
+ prevBackslash = true;
+ i++;
+ continue;
+ }
+
+ if (ch === '"') {
+ inString = !inString;
+ i++;
+ continue;
+ }
+
+ if (!inString) {
+ if (ch === "{") depth++;
+ else if (ch === "}") {
+ depth--;
+ if (depth === 0) {
+ const valueStr = fixed.substring(openBrace, i + 1);
+ try {
+ result[key] = JSON.parse(valueStr);
+ } catch (e) {
+ console.warn(
+ ` Warning: could not parse value for key "${key}" in ${path.basename(filePath)}: ${e.message}. Skipping.`
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ i++;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Merge footer and header data.
+ * Footer is processed first; header entries always overwrite footer entries
+ * (header takes precedence for cross-file duplicates).
+ */
+function mergeData(header, footer) {
+ const result = {};
+
+ for (const [key, value] of Object.entries(footer)) {
+ result[key] = value;
+ }
+
+ for (const [key, value] of Object.entries(header)) {
+ result[key] = value;
+ }
+
+ return result;
+}
+
+/**
+ * Transform a raw JSON record into an Author document.
+ * Trims whitespace from all string values and skips empty strings.
+ */
+function transformRecord(nameKey, record, orgID) {
+ const doc = { nameKey, orgID };
+
+ for (const [srcField, value] of Object.entries(record)) {
+ const destField = FIELD_MAP[srcField.toLowerCase()];
+ if (!destField) continue;
+
+ if (typeof value === "string") {
+ const trimmed = value.trim();
+ if (trimmed) doc[destField] = trimmed;
+ } else if (value !== undefined && value !== null) {
+ doc[destField] = value;
+ }
+ }
+
+ return doc;
+}
+
+export async function runMigration() {
+ const migrationTitle = "Import Authors from JSON Files";
+ try {
+ console.log(`Running migration "${migrationTitle}"...`);
+
+ if (!process.env.MONGOOSEURI) {
+ throw new Error("MONGOOSEURI environment variable is not set.");
+ }
+ if (!process.env.ORG_ID) {
+ throw new Error("ORG_ID environment variable is not set.");
+ }
+
+ await mongoose.connect(process.env.MONGOOSEURI, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ });
+ console.log("Connected to MongoDB.");
+
+ const headerPath = path.resolve(__dirname, "../../prod-authors-header.json");
+ const footerPath = path.resolve(__dirname, "../../prod-authors-footer.json");
+
+ console.log("Parsing prod-authors-header.json (first-wins)...");
+ const headerData = parseFileFirstWins(headerPath);
+ console.log(` ${Object.keys(headerData).length} unique records`);
+
+ console.log("Parsing prod-authors-footer.json (first-wins)...");
+ const footerData = parseFileFirstWins(footerPath);
+ console.log(` ${Object.keys(footerData).length} unique records`);
+
+ const merged = mergeData(headerData, footerData);
+ console.log(`Merged total: ${Object.keys(merged).length} unique records`);
+
+ // Load existing nameKeys to make this migration idempotent
+ const existing = await Author.find({ orgID: process.env.ORG_ID })
+ .select("nameKey")
+ .lean();
+ const existingKeys = new Set(existing.map((a) => a.nameKey));
+ console.log(`Existing authors in DB: ${existingKeys.size}`);
+
+ const toInsert = [];
+ const skippedExisting = [];
+ const skippedInvalid = [];
+
+ for (const [nameKey, record] of Object.entries(merged)) {
+ if (existingKeys.has(nameKey)) {
+ skippedExisting.push(nameKey);
+ continue;
+ }
+
+ const doc = transformRecord(nameKey, record, process.env.ORG_ID);
+
+ if (!doc.name) {
+ console.warn(` Skipping "${nameKey}": missing required "name" field.`);
+ skippedInvalid.push(nameKey);
+ continue;
+ }
+
+ toInsert.push(doc);
+ }
+
+ console.log(`Records to insert: ${toInsert.length}`);
+ console.log(`Records skipped (already in DB): ${skippedExisting.length}`);
+ console.log(`Records skipped (invalid data): ${skippedInvalid.length}`);
+
+ if (toInsert.length === 0) {
+ console.log("Nothing to insert. Migration already complete.");
+ return;
+ }
+
+ // ordered: false — continue inserting even if individual docs fail
+ const insertResult = await Author.insertMany(toInsert, { ordered: false });
+ console.log(`Inserted ${insertResult.length} authors successfully.`);
+ console.log(`Completed migration "${migrationTitle}".`);
+ } catch (e) {
+ // MongoBulkWriteError is thrown by insertMany when ordered:false and some docs fail
+ if (e.name === "MongoBulkWriteError") {
+ console.log(
+ `Inserted ${e.result?.nInserted ?? 0} authors (${e.writeErrors?.length ?? 0} skipped due to write errors).`
+ );
+ return;
+ }
+ console.error(`Fatal error during migration "${migrationTitle}": ${e.toString()}`);
+ throw e;
+ } finally {
+ await mongoose.disconnect();
+ }
+}
+
+// Uncomment to run standalone:
+// runMigration()
+// .then(() => process.exit(0))
+// .catch((e) => {
+// console.error(`Migration failed: ${e.toString()}`);
+// process.exit(1);
+// });
diff --git a/server/migrations/TransformAuthorFields.js b/server/migrations/TransformAuthorFields.js
new file mode 100644
index 000000000..a715d5237
--- /dev/null
+++ b/server/migrations/TransformAuthorFields.js
@@ -0,0 +1,128 @@
+import Author from "../models/author.js";
+import mongoose from "mongoose";
+// import dotenv from 'dotenv';
+// dotenv.config();
+
+/**
+ * Transforms the Author collection to match the new schema:
+ * - Combines firstName + lastName into name
+ * - Renames url to nameURL
+ * - Keeps orgID and userUUID unchanged
+ * - Removes email, primaryInstitution, and isAdminEntry fields
+ *
+ * Idempotent - can be run multiple times safely.
+ */
+export async function runMigration() {
+ const migrationTitle = "Transform Author Fields";
+ try {
+ console.log(`Running migration "${migrationTitle}"...`);
+
+ console.log("Connecting to MongoDB...");
+ if (!process.env.MONGOOSEURI) {
+ throw new Error("MONGOOSEURI environment variable is not set.");
+ }
+
+ await mongoose.connect(process.env.MONGOOSEURI, {
+ useNewUrlParser: true,
+ useUnifiedTopology: true,
+ });
+
+ console.log("Connected to MongoDB.");
+
+ // Find all authors that haven't been migrated yet
+ // (they still have firstName field, which indicates they need migration)
+ const authorsToMigrate = await Author.find({
+ firstName: { $exists: true },
+ }).lean();
+
+ console.log(`Found ${authorsToMigrate.length} authors to migrate.`);
+
+ if (authorsToMigrate.length === 0) {
+ console.log(
+ "No authors need migration. Migration already complete or no authors exist.",
+ );
+ return;
+ }
+
+ // Get the underlying MongoDB collection to bypass Mongoose schema validation
+ const collection = Author.collection;
+
+ const operations = authorsToMigrate.map((author) => {
+ const updateObj = {
+ $set: {},
+ $unset: {},
+ };
+
+ // Generate nameKey from firstName and lastName
+ if (author.firstName || author.lastName) {
+ const firstName = author.firstName || "";
+ const lastName = author.lastName || "";
+ updateObj.$set.nameKey = generateNameKey(firstName, lastName);
+ }
+
+ if (!updateObj.$set.nameKey) {
+ console.warn(
+ `Author ${author._id} is missing both firstName and lastName. Skipping migration for this author.`,
+ );
+ return collection.updateOne({ _id: author._id }, { $set: {} }); // No-op update to skip this author
+ }
+
+ // Combine firstName + lastName into name
+ // Only set if both firstName and lastName exist
+ if (author.firstName || author.lastName) {
+ const firstName = author.firstName || "";
+ const lastName = author.lastName || "";
+ updateObj.$set.name = `${firstName} ${lastName}`.trim();
+ }
+
+ // Rename url to nameURL if it exists
+ if (author.url) {
+ updateObj.$set.nameURL = author.url;
+ }
+
+ // Remove old fields
+ updateObj.$unset.firstName = "";
+ updateObj.$unset.lastName = "";
+ updateObj.$unset.url = "";
+ updateObj.$unset.email = "";
+ updateObj.$unset.primaryInstitution = "";
+ updateObj.$unset.isAdminEntry = "";
+
+ console.log(`Updating author ${author._id}:`, updateObj);
+
+ // Use native MongoDB collection.updateOne() to bypass Mongoose schema validation
+ return collection.updateOne({ _id: author._id }, updateObj);
+ });
+
+ const results = await Promise.all(operations);
+
+ const updatedCount = results.filter((r) => r.modifiedCount > 0).length;
+
+ console.log(`Updated ${updatedCount} authors successfully.`);
+ console.log(`Completed migration "${migrationTitle}".`);
+ } catch (e) {
+ console.error(
+ `Fatal error during migration "${migrationTitle}": ${e.toString()}`,
+ );
+ throw e;
+ } finally {
+ await mongoose.disconnect();
+ }
+}
+
+function generateNameKey(firstName, lastName) {
+ const first = firstName ? firstName.trim() : "";
+ const last = lastName ? lastName.trim() : "";
+
+ return `${first.toLowerCase()}-${last.toLowerCase()}`.trim();
+}
+
+// runMigration()
+// .then(() => {
+// console.log('Migration completed successfully.');
+// process.exit(0);
+// })
+// .catch((e) => {
+// console.error(`Migration failed: ${e.toString()}`);
+// process.exit(1);
+// });
diff --git a/server/models/author.ts b/server/models/author.ts
index b58df1f2a..e16eb56b2 100644
--- a/server/models/author.ts
+++ b/server/models/author.ts
@@ -1,14 +1,21 @@
-import { Schema, model } from "mongoose";
+import { Schema, model, Document } from "mongoose";
export interface AuthorInterface extends Document {
orgID: string;
- firstName: string;
- lastName: string;
- email?: string;
- url?: string;
- primaryInstitution?: string;
- userUUID?: string;
- isAdminEntry?: boolean;
+ nameKey: string; // A normalized version of the name for indexing and searching. Must be unique within an orgID.
+ name: string;
+ nameTitle?: string;
+ nameURL?: string;
+ note?: string;
+ noteURL?: string;
+ companyName?: string;
+ companyURL?: string;
+ pictureCircle?: string; // i.e. "yes" or "no"
+ pictureURL?: string;
+ programName?: string;
+ programURL?: string;
+ attributionPrefix?: string;
+ userUUID?: string; // Optional field if the user uuid of the matching author is known
}
const AuthorSchema = new Schema({
@@ -16,41 +23,65 @@ const AuthorSchema = new Schema({
type: String,
required: true,
},
- firstName: {
+ nameKey: {
type: String,
required: true,
},
- lastName: {
+ name: {
type: String,
required: true,
},
- email: {
+ nameTitle: {
type: String,
required: false,
},
- url: {
+ nameURL: {
type: String,
required: false,
},
- primaryInstitution: {
+ note: {
type: String,
required: false,
},
- userUUID: {
+ noteURL: {
+ type: String,
+ required: false,
+ },
+ companyName: {
+ type: String,
+ required: false,
+ },
+ companyURL: {
+ type: String,
+ required: false,
+ },
+ pictureCircle: {
+ type: String,
+ },
+ pictureURL: {
type: String,
required: false,
},
- isAdminEntry: {
- type: Boolean,
+ programName: {
+ type: String,
+ required: false,
+ },
+ programURL: {
+ type: String,
+ required: false,
+ },
+ attributionPrefix: {
+ type: String,
+ required: false,
+ },
+ userUUID: {
+ type: String,
required: false,
},
});
-// Email is unique, but not required
-AuthorSchema.index(
- { email: 1, orgID: 1},
- { unique: true, partialFilterExpression: { email: { $exists: true } } }
-);
+AuthorSchema.index({ orgID: 1, nameKey: 1 }, { unique: true }); // Ensure nameKey is unique within an orgID
+
const Author = model("Author", AuthorSchema);
diff --git a/server/package-lock.json b/server/package-lock.json
index 3e11c7418..e8588fb27 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -46,7 +46,7 @@
"form-data": "^4.0.4",
"fs-extra": "^10.0.0",
"fuse.js": "^7.0.0",
- "gpt-tokenizer": "^2.8.1",
+ "gpt-tokenizer": "^3.4.0",
"helmet": "^4.1.1",
"isomorphic-dompurify": "^1.9.0",
"jose": "^4.15.5",
@@ -1027,17 +1027,6 @@
}
}
},
- "node_modules/@aws-sdk/util-utf8-browser": {
- "version": "3.259.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz",
- "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.3.1"
- }
- },
"node_modules/@aws-sdk/xml-builder": {
"version": "3.972.10",
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz",
@@ -1442,14 +1431,439 @@
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT",
- "peer": true
+ "peer": true
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
+ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
+ "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
+ "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
+ "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
+ "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
+ "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
+ "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
+ "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
+ "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
+ "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
+ "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
+ "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
+ "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
+ "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
+ "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
+ "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
+ "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
+ "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
+ "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
+ "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
+ "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
+ "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
},
- "node_modules/@emotion/weak-memoize": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
- "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.3",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
+ "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
"license": "MIT",
- "peer": true
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
},
"node_modules/@esbuild/win32-x64": {
"version": "0.27.3",
@@ -2020,81 +2434,403 @@
"resolved": "https://registry.npmjs.org/@node-oauth/oauth2-server/-/oauth2-server-4.3.3.tgz",
"integrity": "sha512-0LiQ6sGwgd+WPQCkayORgv3ju6JNcZsU9IpiXSGCQ/LNlvhQCgwriVaVageXl1J9GwY/rq4wMg6DPwakGZLC2g==",
"license": "MIT",
- "dependencies": {
- "@node-oauth/formats": "1.0.0",
- "basic-auth": "2.0.1",
- "bluebird": "3.7.2",
- "promisify-any": "2.0.1",
- "type-is": "1.6.18"
- },
- "engines": {
- "node": ">=14.0.0"
- }
+ "dependencies": {
+ "@node-oauth/formats": "1.0.0",
+ "basic-auth": "2.0.1",
+ "bluebird": "3.7.2",
+ "promisify-any": "2.0.1",
+ "type-is": "1.6.18"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@qdrant/js-client-rest": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz",
+ "integrity": "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@qdrant/openapi-typescript-fetch": "1.2.6",
+ "undici": "^6.23.0"
+ },
+ "engines": {
+ "node": ">=18.17.0",
+ "pnpm": ">=8"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.7"
+ }
+ },
+ "node_modules/@qdrant/openapi-typescript-fetch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.6.tgz",
+ "integrity": "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0",
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
+ "optional": true,
+ "os": [
+ "linux"
+ ]
},
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">= 8"
- }
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
},
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
"dev": true,
"license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
},
- "node_modules/@qdrant/js-client-rest": {
- "version": "1.17.0",
- "resolved": "https://registry.npmjs.org/@qdrant/js-client-rest/-/js-client-rest-1.17.0.tgz",
- "integrity": "sha512-aZFQeirWVqWAa1a8vJ957LMzcXkFHGbsoRhzc8AkGfg6V0jtK8PlG8/eyyc2xhYsR961FDDx1Tx6nyE0K7lS+A==",
- "license": "Apache-2.0",
- "dependencies": {
- "@qdrant/openapi-typescript-fetch": "1.2.6",
- "undici": "^6.23.0"
- },
- "engines": {
- "node": ">=18.17.0",
- "pnpm": ">=8"
- },
- "peerDependencies": {
- "typescript": ">=4.7"
- }
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
},
- "node_modules/@qdrant/openapi-typescript-fetch": {
- "version": "1.2.6",
- "resolved": "https://registry.npmjs.org/@qdrant/openapi-typescript-fetch/-/openapi-typescript-fetch-1.2.6.tgz",
- "integrity": "sha512-oQG/FejNpItrxRHoyctYvT3rwGZOnK4jr3JdppO/c78ktDvkWiPXPHNsrDf33K9sZdRb6PR7gi4noIapu5q4HA==",
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
"license": "MIT",
- "engines": {
- "node": ">=18.0.0",
- "pnpm": ">=8"
- }
+ "optional": true,
+ "os": [
+ "win32"
+ ]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.59.0",
@@ -4208,9 +4944,9 @@
}
},
"node_modules/cheerio/node_modules/undici": {
- "version": "7.22.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz",
- "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==",
+ "version": "7.24.2",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.2.tgz",
+ "integrity": "sha512-P9J1HWYV/ajFr8uCqk5QixwiRKmB1wOamgS0e+o2Z4A44Ej2+thFVRLG/eA7qprx88XXhnV5Bl8LHXTURpzB3Q==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -6225,6 +6961,21 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
@@ -6500,9 +7251,9 @@
}
},
"node_modules/gpt-tokenizer": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-2.9.0.tgz",
- "integrity": "sha512-YSpexBL/k4bfliAzMrRqn3M6+it02LutVyhVpDeMKrC/O9+pCe/5s8U2hYKa2vFLD5/vHhsKc8sOn/qGqII8Kg==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz",
+ "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==",
"license": "MIT"
},
"node_modules/graceful-fs": {
@@ -7831,48 +8582,6 @@
}
}
},
- "node_modules/langchain/node_modules/@aws-crypto/crc32": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz",
- "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@aws-crypto/util": "^3.0.0",
- "@aws-sdk/types": "^3.222.0",
- "tslib": "^1.11.1"
- }
- },
- "node_modules/langchain/node_modules/@aws-crypto/crc32/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD",
- "optional": true,
- "peer": true
- },
- "node_modules/langchain/node_modules/@aws-crypto/util": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz",
- "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@aws-sdk/types": "^3.222.0",
- "@aws-sdk/util-utf8-browser": "^3.0.0",
- "tslib": "^1.11.1"
- }
- },
- "node_modules/langchain/node_modules/@aws-crypto/util/node_modules/tslib": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
- "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
- "license": "0BSD",
- "optional": true,
- "peer": true
- },
"node_modules/langchain/node_modules/@langchain/community": {
"version": "0.0.57",
"resolved": "https://registry.npmjs.org/@langchain/community/-/community-0.0.57.tgz",
@@ -8307,156 +9016,6 @@
"node": ">=18"
}
},
- "node_modules/langchain/node_modules/@smithy/eventstream-codec": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.2.0.tgz",
- "integrity": "sha512-8janZoJw85nJmQZc4L8TuePp2pk1nxLgkxIR0TUjKJ5Dkj5oelB9WtiSSGXCQvNsJl0VSTvK/2ueMXxvpa9GVw==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@aws-crypto/crc32": "3.0.0",
- "@smithy/types": "^2.12.0",
- "@smithy/util-hex-encoding": "^2.2.0",
- "tslib": "^2.6.2"
- }
- },
- "node_modules/langchain/node_modules/@smithy/is-array-buffer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/protocol-http": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.3.0.tgz",
- "integrity": "sha512-Xy5XK1AFWW2nlY/biWZXu6/krgbaf2dg0q492D8M5qthsnU2H+UgFeZLbM76FnH7s6RO/xhQRkj+T6KBO3JzgQ==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@smithy/types": "^2.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/signature-v4": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.3.0.tgz",
- "integrity": "sha512-ui/NlpILU+6HAQBfJX8BBsDXuKSNrjTSuOYArRblcrErwKFutjrCNb/OExfVRyj9+26F9J+ZmfWT+fKWuDrH3Q==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "@smithy/types": "^2.12.0",
- "@smithy/util-hex-encoding": "^2.2.0",
- "@smithy/util-middleware": "^2.2.0",
- "@smithy/util-uri-escape": "^2.2.0",
- "@smithy/util-utf8": "^2.3.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/types": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz",
- "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/util-buffer-from": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@smithy/is-array-buffer": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/util-hex-encoding": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.2.0.tgz",
- "integrity": "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/util-middleware": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.2.0.tgz",
- "integrity": "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@smithy/types": "^2.12.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/util-uri-escape": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.2.0.tgz",
- "integrity": "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/langchain/node_modules/@smithy/util-utf8": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
- "license": "Apache-2.0",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@smithy/util-buffer-from": "^2.2.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=14.0.0"
- }
- },
"node_modules/langchain/node_modules/@types/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
@@ -12068,9 +12627,9 @@
}
},
"node_modules/undici": {
- "version": "6.23.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
- "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
+ "version": "6.24.1",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
+ "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
"license": "MIT",
"engines": {
"node": ">=18.17"
diff --git a/server/package.json b/server/package.json
index 7a790a261..1dc49d431 100644
--- a/server/package.json
+++ b/server/package.json
@@ -48,7 +48,7 @@
"form-data": "^4.0.4",
"fs-extra": "^10.0.0",
"fuse.js": "^7.0.0",
- "gpt-tokenizer": "^2.8.1",
+ "gpt-tokenizer": "^3.4.0",
"helmet": "^4.1.1",
"isomorphic-dompurify": "^1.9.0",
"jose": "^4.15.5",
diff --git a/server/server.ts b/server/server.ts
index 274fe6e4b..863e63c36 100644
--- a/server/server.ts
+++ b/server/server.ts
@@ -7,6 +7,7 @@ import "dotenv/config";
import path from "path";
import { exit } from "process";
import { fileURLToPath } from "url";
+import fs from "fs";
import express from "express";
import mongoose from "mongoose";
import cookieParser from "cookie-parser";
@@ -125,9 +126,14 @@ app.use("/health", (_req, res) => {
// Serve frontend assets. Use directories relative to server/dist
app.use(express.static(path.join(__dirname, "../../client/dist")));
+// Inject APP_ENV into index.html for frontend use (Vite env variables are not accessible in server code, so we inject at runtime)
+const appEnv = process.env.APP_ENV ?? "production";
+const envScript = ``;
+const indexHtmlPath = path.resolve(__dirname, "../../client/dist/index.html");
+const indexHtml = fs.readFileSync(indexHtmlPath, "utf-8").replace("", `${envScript}`);
let cliRouter = express.Router();
cliRouter.route("*").get((_req, res) => {
- res.sendFile(path.resolve(__dirname, "../../client/dist/index.html"));
+ res.setHeader("Content-Type", "text/html").send(indexHtml);
});
app.use("/", cliRouter);
diff --git a/server/types/Misc.ts b/server/types/Misc.ts
index faf773d11..0ca4b46e2 100644
--- a/server/types/Misc.ts
+++ b/server/types/Misc.ts
@@ -17,6 +17,28 @@ export type License = {
additionalTerms?: string;
};
+export type ConductorInfiniteScrollResponse = ({
+ err: false;
+} & BaseConductorInfiniteScrollResponse) | {
+ err: true;
+ errMsg: string;
+};
+
+/**
+ * The base data structure for responses from the Conductor server when fetching items with infinite scroll pagination.
+ * The controller method should return data in the `ConductorInfiniteScrollResponse` format which wraps this base structure and adds an `err` field to indicate success or failure of the request.
+ * This type is used to ensure consistent response formats across different API endpoints that implement infinite scroll pagination.
+ */
+export type BaseConductorInfiniteScrollResponse = {
+ items: T[];
+ meta: {
+ total_count: number;
+ has_more: boolean;
+ next_page: string | number | null;
+ }
+}
+
+
/**
* A TypeScript type alias called `Prettify`.
* It takes a type as its argument and returns a new type that has the same properties as the original type,
diff --git a/server/util/helpers.js b/server/util/helpers.js
index bad6cdbdc..8918d175e 100644
--- a/server/util/helpers.js
+++ b/server/util/helpers.js
@@ -388,6 +388,16 @@ export function sanitizeControlCharacters(str) {
return str.replace(/\p{C}/gu, '');
}
+/**
+ * Escapes special characters in a string for use in a regular expression.
+ * @param {*} input - The string to escape.
+ * @returns {*} The escaped string, or the original value if not a string.
+ */
+export function escapeRegEx(input) {
+ if (typeof input !== 'string') return input;
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
/**
* Decodes a percent-encoded string if applicable, otherwise returns the original string.
* @param {*} str - The string to decode.