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:
diegosouzapw
2026-04-08 03:15:42 -03:00
parent 39eb5a50ab
commit 0da777683f
3 changed files with 62 additions and 7 deletions

View File

@@ -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 }
);
}

View File

@@ -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>
);
}

View File

@@ -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 1822 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",