monitor/core/modules/WebServer/getReactIndex.ts
2025-04-16 22:30:27 +07:00

198 lines
7.9 KiB
TypeScript

const modulename = 'WebCtxUtils';
import fsp from "node:fs/promises";
import path from "node:path";
import type { InjectedTxConsts, ThemeType } from '@shared/otherTypes';
import { txEnv, txDevEnv, txHostConfig } from "@core/globalData";
import { AuthedCtx, CtxWithVars } from "./ctxTypes";
import consts from "@shared/consts";
import consoleFactory from '@lib/console';
import { AuthedAdminType, checkRequestAuth } from "./authLogic";
import { isString } from "@modules/CacheStore";
const console = consoleFactory(modulename);
// NOTE: it's not possible to remove the hardcoded import of the entry point in the index.html file
// even if you set the entry point manually in the vite config.
// Therefore, it was necessary to tag it with `data-prod-only` so it can be removed in dev mode.
//Consts
const serverTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
//Cache the index.html file unless in dev mode
let htmlFile: string;
// NOTE: https://vitejs.dev/guide/backend-integration.html
const viteOrigin = txDevEnv.VITE_URL ?? 'doesnt-matter';
const devModulesScript = `<script type="module">
import { injectIntoGlobalHook } from "${viteOrigin}/@react-refresh";
injectIntoGlobalHook(window);
window.$RefreshReg$ = () => {};
window.$RefreshSig$ = () => (type) => type;
window.__vite_plugin_react_preamble_installed__ = true;
</script>
<script type="module" src="${viteOrigin}/@vite/client"></script>
<script type="module" src="${viteOrigin}/src/main.tsx"></script>`;
//Custom themes placeholder
export const tmpDefaultTheme = 'dark';
export const tmpDefaultThemes = ['dark', 'light'];
export const tmpCustomThemes: ThemeType[] = [
// {
// name: 'deep-purple',
// isDark: true,
// style: {
// "background": "274 93% 39%",
// "foreground": "269 9% 100%",
// "card": "274 79% 53%",
// "card-foreground": "270 48% 99%",
// "popover": "240 10% 3.9%",
// "popover-foreground": "270 48% 99%",
// "primary": "270 48% 99%",
// "primary-foreground": "240 5.9% 10%",
// "secondary": "240 3.7% 15.9%",
// "secondary-foreground": "270 48% 99%",
// "muted": "240 3.7% 15.9%",
// "muted-foreground": "240 5% 64.9%",
// "accent": "240 3.7% 15.9%",
// "accent-foreground": "270 48% 99%",
// "destructive": "0 62.8% 30.6%",
// "destructive-foreground": "270 48% 99%",
// "border": "273 79%, 53%",
// "input": "240 3.7% 15.9%",
// "ring": "240 4.9% 83.9%",
// }
// }
];
/**
* Returns the react index.html file with placeholders replaced
* FIXME: add favicon
*/
export default async function getReactIndex(ctx: CtxWithVars | AuthedCtx) {
//Read file if not cached
if (txDevEnv.ENABLED || !htmlFile) {
try {
const indexPath = txDevEnv.ENABLED
? path.join(txDevEnv.SRC_PATH, '/panel/index.html')
: path.join(txEnv.txaPath, 'panel/index.html')
const rawHtmlFile = await fsp.readFile(indexPath, 'utf-8');
//Remove tagged lines (eg hardcoded entry point) depending on env
if (txDevEnv.ENABLED) {
htmlFile = rawHtmlFile.replaceAll(/.+data-prod-only.+\r?\n/gm, '');
} else {
htmlFile = rawHtmlFile.replaceAll(/.+data-dev-only.+\r?\n/gm, '');
}
} catch (error) {
if ((error as any).code == 'ENOENT') {
return `<h1>⚠ index.html not found:</h1><pre>You probably deleted the 'citizen/system_resources/monitor/panel/index.html' file, or the folders above it.</pre>`;
} else {
return `<h1>⚠ index.html load error:</h1><pre>${(error as Error).message}</pre>`
}
}
}
//Checking if already logged in
const authResult = checkRequestAuth(
ctx.request.headers,
ctx.ip,
ctx.txVars.isLocalRequest,
ctx.sessTools
);
let authedAdmin: AuthedAdminType | false = false;
if (authResult.success) {
authedAdmin = authResult.admin;
}
//Preparing vars
const basePath = (ctx.txVars.isWebInterface) ? '/' : consts.nuiWebpipePath;
const injectedConsts: InjectedTxConsts = {
//env
fxsVersion: txEnv.fxsVersionTag,
fxsOutdated: txCore.updateChecker.fxsUpdateData,
txaVersion: txEnv.txaVersion,
txaOutdated: txCore.updateChecker.txaUpdateData,
serverTimezone,
isWindows: txEnv.isWindows,
isWebInterface: ctx.txVars.isWebInterface,
showAdvanced: (txDevEnv.ENABLED || console.isVerbose),
hasMasterAccount: txCore.adminStore.hasAdmins(true),
defaultTheme: tmpDefaultTheme,
customThemes: tmpCustomThemes.map(({ name, isDark }) => ({ name, isDark })),
adsData: txEnv.adsData,
providerLogo: txHostConfig.providerLogo,
providerName: txHostConfig.providerName,
hostConfigSource: txHostConfig.sourceName,
//Login page info
server: {
name: txCore.cacheStore.getTyped('fxsRuntime:projectName', isString) ?? txConfig.general.serverName,
game: txCore.cacheStore.getTyped('fxsRuntime:gameName', isString),
icon: txCore.cacheStore.getTyped('fxsRuntime:iconFilename', isString),
},
//auth
preAuth: authedAdmin && authedAdmin.getAuthData(),
};
//Prepare placeholders
const replacers: { [key: string]: string } = {};
replacers.basePath = `<base href="${basePath}">`;
replacers.ogTitle = `txAdmin - ${txConfig.general.serverName}`;
replacers.ogDescripttion = `Manage & Monitor your FiveM/RedM Server with txAdmin v${txEnv.txaVersion} atop FXServer ${txEnv.fxsVersion}`;
replacers.txConstsInjection = `<script>window.txConsts = ${JSON.stringify(injectedConsts)};</script>`;
replacers.devModules = txDevEnv.ENABLED ? devModulesScript : '';
//Prepare custom themes style tag
if (tmpCustomThemes.length) {
const cssThemes = [];
for (const theme of tmpCustomThemes) {
const cssVars = [];
for (const [name, value] of Object.entries(theme.style)) {
cssVars.push(`--${name}: ${value};`);
}
cssThemes.push(`.theme-${theme.name} { ${cssVars.join(' ')} }`);
}
replacers.customThemesStyle = `<style>${cssThemes.join('\n')}</style>`;
} else {
replacers.customThemesStyle = '';
}
//Setting the theme class from the cookie
const themeCookie = ctx.cookies.get('txAdmin-theme');
if (themeCookie) {
if (tmpDefaultThemes.includes(themeCookie)) {
replacers.htmlClasses = themeCookie;
} else {
const selectedCustomTheme = tmpCustomThemes.find((theme) => theme.name === themeCookie);
if (!selectedCustomTheme) {
replacers.htmlClasses = tmpDefaultTheme;
} else {
const lightDarkSelector = selectedCustomTheme.isDark ? 'dark' : 'light';
replacers.htmlClasses = `${lightDarkSelector} theme-${selectedCustomTheme.name}`;
}
}
} else {
replacers.htmlClasses = tmpDefaultTheme;
}
//Replace
let htmlOut = htmlFile;
for (const [placeholder, value] of Object.entries(replacers)) {
const replacerRegex = new RegExp(`(<!--\\s*)?{{${placeholder}}}(\\s*-->)?`, 'g');
htmlOut = htmlOut.replaceAll(replacerRegex, value);
}
//If in prod mode and NUI, replace the entry point with the local one
//This is required because of how badly the WebPipe handles "large" files
if (!txDevEnv.ENABLED) {
const base = ctx.txVars.isWebInterface ? `./` : `nui://monitor/panel/`;
htmlOut = htmlOut.replace(/src="\.\/index-(\w+(?:\.v\d+)?)\.js"/, `src="${base}index-$1.js"`);
htmlOut = htmlOut.replace(/href="\.\/index-(\w+(?:\.v\d+)?)\.css"/, `href="${base}index-$1.css"`);
}
return htmlOut;
}