mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(login): show Node.js version incompatibility warning on login page
When running OmniRoute with Node.js >=24, the better-sqlite3 native module fails to load, causing a confusing 'not a valid Win32 application' or 'Module did not self-register' error at the login screen. This change adds proactive Node.js version detection: - API: /api/settings/require-login now returns nodeVersion and nodeCompatible fields, computed before any SQLite access so they work even when the DB fails - UI: A prominent red warning banner appears on the login page when an incompatible Node.js version is detected, showing the current version and instructions to install Node 22 LTS via nvm - i18n: Added 4 new translation keys for the warning banner Closes #1060, Closes #1040
This commit is contained in:
@@ -4,17 +4,25 @@ import bcrypt from "bcryptjs";
|
||||
import { updateRequireLoginSchema } from "@/shared/validation/schemas";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
|
||||
// Node.js compatibility check — better-sqlite3 requires Node <24
|
||||
function getNodeCompatibility() {
|
||||
const nodeVersion = process.version;
|
||||
const major = parseInt(nodeVersion.replace("v", "").split(".")[0], 10);
|
||||
return { nodeVersion, nodeCompatible: major >= 18 && major < 24 };
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
const nodeInfo = getNodeCompatibility();
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
const requireLogin = settings.requireLogin !== false;
|
||||
const hasPassword = !!settings.password || !!process.env.INITIAL_PASSWORD;
|
||||
const setupComplete = !!settings.setupComplete;
|
||||
return NextResponse.json({ requireLogin, hasPassword, setupComplete });
|
||||
return NextResponse.json({ requireLogin, hasPassword, setupComplete, ...nodeInfo });
|
||||
} catch (error) {
|
||||
console.error("[API] Error fetching require-login settings:", error);
|
||||
return NextResponse.json(
|
||||
{ requireLogin: true, hasPassword: true, setupComplete: true },
|
||||
{ requireLogin: true, hasPassword: true, setupComplete: true, ...nodeInfo },
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export default function LoginPage() {
|
||||
const [hasPassword, setHasPassword] = useState(null);
|
||||
const [setupComplete, setSetupComplete] = useState(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [nodeVersion, setNodeVersion] = useState(null);
|
||||
const [nodeCompatible, setNodeCompatible] = useState(true);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -31,6 +33,8 @@ export default function LoginPage() {
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.nodeVersion) setNodeVersion(data.nodeVersion);
|
||||
if (data.nodeCompatible === false) setNodeCompatible(false);
|
||||
if (data.requireLogin === false) {
|
||||
router.push("/dashboard");
|
||||
router.refresh();
|
||||
@@ -83,9 +87,39 @@ export default function LoginPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const nodeWarningBanner = !nodeCompatible && nodeVersion ? (
|
||||
<div className="w-full max-w-lg mx-auto mb-6 animate-in fade-in slide-in-from-top-2 duration-500">
|
||||
<div className="bg-red-950/60 border-2 border-red-500/40 rounded-2xl p-6 shadow-lg shadow-red-900/20 backdrop-blur-sm">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-red-500/20 flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="material-symbols-outlined text-red-400 text-[28px]">error</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-base font-bold text-red-300 mb-1">{t("nodeIncompatibleTitle")}</h3>
|
||||
<p className="text-sm text-red-200/80 leading-relaxed mb-3">
|
||||
{t("nodeIncompatibleDesc", { version: nodeVersion })}
|
||||
</p>
|
||||
<div className="bg-black/40 rounded-lg px-4 py-3 font-mono text-sm border border-red-500/20">
|
||||
<div className="flex items-center gap-2 text-red-300/60 mb-1">
|
||||
<span className="material-symbols-outlined text-[14px]">terminal</span>
|
||||
<span className="text-xs">{t("nodeIncompatibleFixLabel")}</span>
|
||||
</div>
|
||||
<code className="text-amber-300">nvm install 22 && nvm use 22</code>
|
||||
</div>
|
||||
<p className="text-xs text-red-300/50 mt-3 flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined text-[14px]">info</span>
|
||||
{t("nodeIncompatibleHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
if (hasPassword === null || setupComplete === null) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg">
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-bg p-6">
|
||||
{nodeWarningBanner}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 border-2 border-primary/20 rounded-full"></div>
|
||||
@@ -99,7 +133,8 @@ export default function LoginPage() {
|
||||
|
||||
if (!hasPassword && !setupComplete) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg p-6">
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-bg p-6">
|
||||
{nodeWarningBanner}
|
||||
<div
|
||||
className={`w-full max-w-md transition-all duration-700 ease-out ${mounted ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
||||
>
|
||||
@@ -136,7 +171,8 @@ export default function LoginPage() {
|
||||
|
||||
if (!hasPassword && setupComplete) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-bg p-6">
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-bg p-6">
|
||||
{nodeWarningBanner}
|
||||
<div
|
||||
className={`w-full max-w-md transition-all duration-700 ease-out ${mounted ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
||||
>
|
||||
@@ -174,7 +210,13 @@ export default function LoginPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex bg-bg">
|
||||
<div className="min-h-screen flex flex-col bg-bg">
|
||||
{nodeWarningBanner && (
|
||||
<div className="flex justify-center pt-6 px-6">
|
||||
{nodeWarningBanner}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 flex bg-bg">
|
||||
<div className="flex-1 flex items-center justify-center p-6">
|
||||
<div
|
||||
className={`w-full max-w-sm transition-all duration-700 ease-out ${mounted ? "opacity-100 translate-y-0" : "opacity-0 translate-y-4"}`}
|
||||
@@ -279,6 +321,7 @@ export default function LoginPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2709,7 +2709,11 @@
|
||||
"waitingForOpenAIAuthorization": "Waiting for OpenAI authorization...",
|
||||
"waitingForAntigravityAuthorization": "Waiting for Antigravity authorization...",
|
||||
"waitingForQoderAuthorization": "Waiting for Qoder authorization...",
|
||||
"exchangingCodeForTokens": "Exchanging code for tokens..."
|
||||
"exchangingCodeForTokens": "Exchanging code for tokens...",
|
||||
"nodeIncompatibleTitle": "Incompatible Node.js Version",
|
||||
"nodeIncompatibleDesc": "You are running Node.js {version}, which is not compatible with OmniRoute. The native SQLite module (better-sqlite3) requires Node.js 18–22 LTS.",
|
||||
"nodeIncompatibleFixLabel": "Fix: install Node.js 22 LTS",
|
||||
"nodeIncompatibleHint": "OmniRoute requires Node.js ≥18 and <24. Node 22 LTS is recommended for stability."
|
||||
},
|
||||
"landing": {
|
||||
"brandName": "OmniRoute",
|
||||
|
||||
Reference in New Issue
Block a user