Fix electron version checking

This commit is contained in:
LukeGus
2025-09-28 19:00:43 -05:00
parent 67ab3e50ff
commit 60b4040f11
13 changed files with 1783 additions and 144 deletions

View File

@@ -920,17 +920,25 @@ router.post("/login", async (req, res) => {
dataUnlocked: true,
});
const response: any = {
success: true,
is_admin: !!userRecord.is_admin,
username: userRecord.username,
};
const isElectron = req.headers['x-electron-app'] === 'true' || req.headers['X-Electron-App'] === 'true';
if (isElectron) {
response.token = token;
}
return res
.cookie(
"jwt",
token,
authManager.getSecureCookieOptions(req, 24 * 60 * 60 * 1000),
)
.json({
success: true,
is_admin: !!userRecord.is_admin,
username: userRecord.username,
});
.json(response);
} catch (err) {
authLogger.error("Failed to log in user", err);
return res.status(500).json({ error: "Login failed" });
@@ -1499,6 +1507,7 @@ router.post("/totp/verify-login", async (req, res) => {
success: true,
is_admin: !!userRecord.is_admin,
username: userRecord.username,
token: req.headers['x-electron-app'] === 'true' ? token : undefined,
});
} catch (err) {
authLogger.error("TOTP verification failed", err);

View File

@@ -1,7 +1,7 @@
import React from "react";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
import { Button } from "@/components/ui/button.tsx";
import { ExternalLink, Download, X, AlertTriangle } from "lucide-react";
import { ExternalLink, Download, AlertTriangle } from "lucide-react";
import { useTranslation } from "react-i18next";
interface VersionAlertProps {
@@ -21,16 +21,12 @@ interface VersionAlertProps {
cache_age?: number;
error?: string;
};
onDismiss?: () => void;
onDownload?: () => void;
showDismiss?: boolean;
}
export function VersionAlert({
updateInfo,
onDismiss,
onDownload,
showDismiss = true,
}: VersionAlertProps) {
const { t } = useTranslation();
@@ -106,18 +102,6 @@ export function VersionAlert({
{t("versionCheck.downloadUpdate")}
</Button>
)}
{showDismiss && onDismiss && (
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="flex items-center gap-1"
>
<X className="h-3 w-3" />
{t("versionCheck.dismiss")}
</Button>
)}
</div>
</AlertDescription>
</Alert>

View File

@@ -8,9 +8,10 @@ import { checkElectronUpdate, isElectron } from "@/ui/main-axios.ts";
interface VersionCheckModalProps {
onDismiss: () => void;
onContinue: () => void;
isAuthenticated?: boolean;
}
export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalProps) {
export function VersionCheckModal({ onDismiss, onContinue, isAuthenticated = false }: VersionCheckModalProps) {
const { t } = useTranslation();
const [versionInfo, setVersionInfo] = useState<any>(null);
const [versionChecking, setVersionChecking] = useState(false);
@@ -29,6 +30,11 @@ export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalPr
try {
const updateInfo = await checkElectronUpdate();
setVersionInfo(updateInfo);
if (updateInfo?.status === "up_to_date") {
onContinue();
return;
}
} catch (error) {
console.error("Failed to check for updates:", error);
setVersionInfo({ success: false, error: "Check failed" });
@@ -57,8 +63,25 @@ export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalPr
if (versionChecking && !versionInfo) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && (
<div
className="absolute inset-0"
style={{
backgroundImage: `linear-gradient(
135deg,
transparent 0%,
transparent 49%,
rgba(255, 255, 255, 0.03) 49%,
rgba(255, 255, 255, 0.03) 51%,
transparent 51%,
transparent 100%
)`,
backgroundSize: "80px 80px",
}}
/>
)}
<div className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<div className="flex items-center justify-center mb-4">
<div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
@@ -70,53 +93,47 @@ export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalPr
);
}
if (!versionInfo || versionInfo.status === "up_to_date" || versionDismissed) {
if (!versionInfo || versionDismissed) {
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && (
<div
className="absolute inset-0"
style={{
backgroundImage: `linear-gradient(
135deg,
transparent 0%,
transparent 49%,
rgba(255, 255, 255, 0.03) 49%,
rgba(255, 255, 255, 0.03) 51%,
transparent 51%,
transparent 100%
)`,
backgroundSize: "80px 80px",
}}
/>
)}
<div className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<div className="mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.checkUpdates")}
</h2>
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
{versionInfo && !versionDismissed && (
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
)}
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="flex-1"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
{t("versionCheck.refresh")}
</Button>
<Button
onClick={handleContinue}
className="flex-1"
className="flex-1 h-10"
>
{t("common.continue")}
</Button>
@@ -127,49 +144,42 @@ export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalPr
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-dark-bg border border-dark-border rounded-lg p-6 max-w-md w-full mx-4">
<div className="flex items-center justify-between mb-4">
<div className="fixed inset-0 flex items-center justify-center z-50">
{!isAuthenticated && (
<div
className="absolute inset-0"
style={{
backgroundImage: `linear-gradient(
135deg,
transparent 0%,
transparent 49%,
rgba(255, 255, 255, 0.03) 49%,
rgba(255, 255, 255, 0.03) 51%,
transparent 51%,
transparent 100%
)`,
backgroundSize: "80px 80px",
}}
/>
)}
<div className="bg-dark-bg border-2 border-dark-border rounded-lg p-6 max-w-md w-full mx-4 relative z-10">
<div className="mb-4">
<h2 className="text-lg font-semibold">
{t("versionCheck.updateRequired")}
</h2>
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
<div className="mb-4">
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="flex-1"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
{t("versionCheck.refresh")}
</Button>
<Button
onClick={handleContinue}
className="flex-1"
className="flex-1 h-10"
>
{t("common.continue")}
</Button>

View File

@@ -229,13 +229,16 @@
"checkUpdates": "Check for Updates",
"checkingUpdates": "Checking for updates...",
"refresh": "Refresh",
"updateRequired": "Update Required"
"updateRequired": "Update Required",
"updateDismissed": "Update notification dismissed",
"noUpdatesFound": "No updates found"
},
"common": {
"close": "Close",
"minimize": "Minimize",
"online": "Online",
"offline": "Offline",
"continue": "Continue",
"maintenance": "Maintenance",
"degraded": "Degraded",
"discord": "Discord",

View File

@@ -227,13 +227,16 @@
"checkUpdates": "检查更新",
"checkingUpdates": "正在检查更新...",
"refresh": "刷新",
"updateRequired": "需要更新"
"updateRequired": "需要更新",
"updateDismissed": "更新通知已忽略",
"noUpdatesFound": "未找到更新"
},
"common": {
"close": "关闭",
"minimize": "最小化",
"online": "在线",
"offline": "离线",
"continue": "继续",
"maintenance": "维护中",
"degraded": "降级",
"discord": "Discord",

View File

@@ -273,7 +273,7 @@ export function AdminSettings({
try {
const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/export`
: "http://localhost:30001/database/export";
: "/database/export";
const response = await fetch(apiUrl, {
method: "POST",
@@ -333,7 +333,7 @@ export function AdminSettings({
try {
const apiUrl = isElectron()
? `${(window as any).configuredServerUrl}/database/import`
: "http://localhost:30001/database/import";
: "/database/import";
const formData = new FormData();
formData.append("file", importFile);

View File

@@ -100,10 +100,11 @@ function AppContent() {
<VersionCheckModal
onDismiss={() => setShowVersionCheck(false)}
onContinue={() => setShowVersionCheck(false)}
isAuthenticated={isAuthenticated}
/>
)}
{!isAuthenticated && !authLoading && (
{!isAuthenticated && !authLoading && !showVersionCheck && (
<div>
<div
className="absolute inset-0"
@@ -123,7 +124,7 @@ function AppContent() {
</div>
)}
{!isAuthenticated && !authLoading && (
{!isAuthenticated && !authLoading && !showVersionCheck && (
<div className="fixed inset-0 flex items-center justify-center z-[10000]">
<Homepage
onSelectView={handleSelectView}

View File

@@ -696,7 +696,7 @@ export function HomepageAuth({
</div>
)}
{!internalLoggedIn && !authLoading && !totpRequired && (
{!loggedIn && !authLoading && !totpRequired && (
<>
<div className="flex gap-2 mb-6">
<button
@@ -963,7 +963,7 @@ export function HomepageAuth({
className="h-11 text-base"
value={localUsername}
onChange={(e) => setLocalUsername(e.target.value)}
disabled={loading || internalLoggedIn}
disabled={loading || loggedIn}
/>
</div>
<div className="flex flex-col gap-2">
@@ -974,7 +974,7 @@ export function HomepageAuth({
className="h-11 text-base"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading || internalLoggedIn}
disabled={loading || loggedIn}
/>
</div>
{tab === "signup" && (
@@ -988,7 +988,7 @@ export function HomepageAuth({
className="h-11 text-base"
value={signupConfirmPassword}
onChange={(e) => setSignupConfirmPassword(e.target.value)}
disabled={loading || internalLoggedIn}
disabled={loading || loggedIn}
/>
</div>
)}
@@ -1008,7 +1008,7 @@ export function HomepageAuth({
type="button"
variant="outline"
className="w-full h-11 text-base font-semibold"
disabled={loading || internalLoggedIn}
disabled={loading || loggedIn}
onClick={() => {
setTab("reset");
resetPasswordState();

View File

@@ -142,7 +142,7 @@ function createApiInstance(
baseURL,
headers: { "Content-Type": "application/json" },
timeout: 30000,
withCredentials: true, // Required for HttpOnly cookies to be sent cross-origin
withCredentials: true,
});
instance.interceptors.request.use((config) => {
@@ -168,13 +168,14 @@ function createApiInstance(
if (process.env.NODE_ENV === "development") {
logger.requestStart(method, fullUrl, context);
}
// Note: JWT token is now automatically sent via secure HttpOnly cookies
// No need to manually set Authorization header for cookie-based auth
if (isElectron()) {
config.headers["X-Electron-App"] = "true";
config.headers["User-Agent"] = "Termix-Electron/1.6.0";
const token = localStorage.getItem("jwt");
if (token) {
config.headers["Authorization"] = `Bearer ${token}`;
}
}
return config;
@@ -300,6 +301,10 @@ function createApiInstance(
// ============================================================================
function isDev(): boolean {
if (isElectron()) {
return false;
}
return (
process.env.NODE_ENV === "development" &&
(window.location.port === "3000" ||
@@ -347,6 +352,7 @@ export async function saveServerConfig(config: ServerConfig): Promise<boolean> {
);
if (result?.success) {
configuredServerUrl = config.serverUrl;
(window as any).configuredServerUrl = configuredServerUrl;
updateApiInstances();
return true;
}
@@ -405,18 +411,8 @@ export async function checkElectronUpdate(): Promise<{
}
}
if (isElectron()) {
getServerConfig().then((config) => {
if (config?.serverUrl) {
configuredServerUrl = config.serverUrl;
updateApiInstances();
}
});
}
function getApiUrl(path: string, defaultPort: number): string {
if (isDev()) {
// Auto-detect HTTPS in development
const protocol = window.location.protocol === "https:" ? "https" : "http";
const sslPort = protocol === "https" ? 8443 : defaultPort;
return `${protocol}://${apiHost}:${sslPort}${path}`;
@@ -466,7 +462,20 @@ export let statsApi: AxiosInstance;
// Authentication API (port 30001)
export let authApi: AxiosInstance;
initializeApiInstances();
if (isElectron()) {
getServerConfig().then((config) => {
if (config?.serverUrl) {
configuredServerUrl = config.serverUrl;
(window as any).configuredServerUrl = configuredServerUrl;
}
initializeApiInstances();
}).catch((error) => {
console.error("Failed to load server config, initializing with default:", error);
initializeApiInstances();
});
} else {
initializeApiInstances();
}
function updateApiInstances() {
systemLogger.info("Updating API instances with new server configuration", {
@@ -1518,8 +1527,13 @@ export async function loginUser(
): Promise<AuthResponse> {
try {
const response = await authApi.post("/users/login", { username, password });
if (isElectron() && response.data.token) {
localStorage.setItem("jwt", response.data.token);
}
return {
token: "cookie-based",
token: response.data.token || "cookie-based",
success: response.data.success,
is_admin: response.data.is_admin,
username: response.data.username,