Update electron builds, fix backend issues
This commit is contained in:
@@ -9,7 +9,6 @@ COPY package*.json ./
|
||||
ENV npm_config_target_platform=linux
|
||||
ENV npm_config_target_arch=x64
|
||||
ENV npm_config_target_libc=glibc
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN npm ci --force --ignore-scripts && \
|
||||
npm cache clean --force
|
||||
@@ -20,11 +19,7 @@ WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm run build
|
||||
RUN npm run build:types
|
||||
|
||||
# Stage 3: Build backend TypeScript
|
||||
FROM deps AS backend-builder
|
||||
@@ -35,8 +30,6 @@ COPY . .
|
||||
ENV npm_config_target_platform=linux
|
||||
ENV npm_config_target_arch=x64
|
||||
ENV npm_config_target_libc=glibc
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
ENV NODE_ENV=production
|
||||
|
||||
RUN npm rebuild better-sqlite3 --force
|
||||
|
||||
@@ -46,14 +39,13 @@ RUN npm run build:backend
|
||||
FROM node:24-alpine AS production-deps
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
RUN apk add --no-cache python3 make g++
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
ENV npm_config_target_platform=linux
|
||||
ENV npm_config_target_arch=x64
|
||||
ENV npm_config_target_libc=glibc
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
RUN npm ci --only=production --ignore-scripts --force && \
|
||||
npm rebuild better-sqlite3 bcryptjs --force && \
|
||||
@@ -90,4 +82,4 @@ EXPOSE ${PORT} 30001 30002 30003 30004 30005
|
||||
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
CMD ["/entrypoint.sh"]
|
||||
CMD ["/entrypoint.sh"]
|
||||
@@ -32,11 +32,9 @@ if [ "$ENABLE_SSL" = "true" ]; then
|
||||
|
||||
DOMAIN=${SSL_DOMAIN:-localhost}
|
||||
|
||||
# Check if certificates already exist and are valid
|
||||
if [ -f "/app/data/ssl/termix.crt" ] && [ -f "/app/data/ssl/termix.key" ]; then
|
||||
echo "SSL certificates found, checking validity..."
|
||||
|
||||
# Check if certificate is still valid (not expired within 30 days)
|
||||
if openssl x509 -in /app/data/ssl/termix.crt -checkend 2592000 -noout >/dev/null 2>&1; then
|
||||
echo "SSL certificates are valid and will be reused for domain: $DOMAIN"
|
||||
else
|
||||
@@ -47,7 +45,6 @@ if [ "$ENABLE_SSL" = "true" ]; then
|
||||
echo "SSL certificates not found, will generate new ones..."
|
||||
fi
|
||||
|
||||
# Generate certificates only if they don't exist or are invalid
|
||||
if [ ! -f "/app/data/ssl/termix.crt" ] || [ ! -f "/app/data/ssl/termix.key" ]; then
|
||||
echo "Generating SSL certificates for domain: $DOMAIN"
|
||||
|
||||
@@ -101,6 +98,18 @@ echo "Starting backend services..."
|
||||
cd /app
|
||||
export NODE_ENV=production
|
||||
|
||||
if [ -f "package.json" ]; then
|
||||
VERSION=$(grep '"version"' package.json | sed 's/.*"version": *"\([^"]*\)".*/\1/')
|
||||
if [ -n "$VERSION" ]; then
|
||||
export VERSION
|
||||
echo "Detected version: $VERSION"
|
||||
else
|
||||
echo "Warning: Could not extract version from package.json"
|
||||
fi
|
||||
else
|
||||
echo "Warning: package.json not found"
|
||||
fi
|
||||
|
||||
if command -v su-exec > /dev/null 2>&1; then
|
||||
su-exec node node dist/backend/backend/starter.js
|
||||
else
|
||||
|
||||
@@ -126,26 +126,31 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const req = client.request(
|
||||
url,
|
||||
{
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 10000,
|
||||
},
|
||||
(res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 10000,
|
||||
};
|
||||
|
||||
if (isHttps) {
|
||||
requestOptions.rejectUnauthorized = false;
|
||||
requestOptions.agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
}
|
||||
|
||||
const req = client.request(url, requestOptions, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
@@ -295,26 +300,31 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
||||
const isHttps = urlObj.protocol === "https:";
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
const req = client.request(
|
||||
url,
|
||||
{
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 5000,
|
||||
},
|
||||
(res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
const requestOptions = {
|
||||
method: options.method || "GET",
|
||||
headers: options.headers || {},
|
||||
timeout: options.timeout || 5000,
|
||||
};
|
||||
|
||||
if (isHttps) {
|
||||
requestOptions.rejectUnauthorized = false;
|
||||
requestOptions.agent = new https.Agent({
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
}
|
||||
|
||||
const req = client.request(url, requestOptions, (res) => {
|
||||
let data = "";
|
||||
res.on("data", (chunk) => (data += chunk));
|
||||
res.on("end", () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
status: res.statusCode,
|
||||
text: () => Promise.resolve(data),
|
||||
json: () => Promise.resolve(JSON.parse(data)),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
req.on("error", reject);
|
||||
req.on("timeout", () => {
|
||||
|
||||
1515
package-lock.json
generated
1515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,9 +10,7 @@
|
||||
"clean": "npx prettier . --write",
|
||||
"dev": "vite",
|
||||
"dev:https": "cross-env VITE_HTTPS=true vite",
|
||||
"build": "vite build",
|
||||
"build:types": "tsc -p tsconfig.node.json",
|
||||
"build:full": "npm run build && npm run build:types",
|
||||
"build": "vite build && tsc -p tsconfig.node.json",
|
||||
"build:backend": "tsc -p tsconfig.node.json",
|
||||
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
||||
"start": "npm run build:backend && node ./dist/backend/backend/starter.js",
|
||||
@@ -132,4 +130,4 @@
|
||||
"typescript-eslint": "^8.40.0",
|
||||
"vite": "^7.1.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,14 +218,46 @@ app.get("/version", authenticateJWT, async (req, res) => {
|
||||
let localVersion = process.env.VERSION;
|
||||
|
||||
if (!localVersion) {
|
||||
try {
|
||||
const packagePath = path.resolve(process.cwd(), "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
localVersion = packageJson.version;
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to read version from package.json", error, {
|
||||
operation: "version_check",
|
||||
});
|
||||
const versionSources = [
|
||||
() => {
|
||||
try {
|
||||
const packagePath = path.resolve(process.cwd(), "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
const packagePath = path.resolve("/app", "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
const packagePath = path.resolve(__dirname, "../../../package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const getVersion of versionSources) {
|
||||
try {
|
||||
const foundVersion = getVersion();
|
||||
if (foundVersion && foundVersion !== "unknown") {
|
||||
localVersion = foundVersion;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -126,15 +126,18 @@ async function initializeCompleteDatabase(): Promise<void> {
|
||||
password_hash TEXT NOT NULL,
|
||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||
is_oidc INTEGER NOT NULL DEFAULT 0,
|
||||
client_id TEXT NOT NULL,
|
||||
client_secret TEXT NOT NULL,
|
||||
issuer_url TEXT NOT NULL,
|
||||
authorization_url TEXT NOT NULL,
|
||||
token_url TEXT NOT NULL,
|
||||
redirect_uri TEXT,
|
||||
identifier_path TEXT NOT NULL,
|
||||
name_path TEXT NOT NULL,
|
||||
scopes TEXT NOT NULL
|
||||
oidc_identifier TEXT,
|
||||
client_id TEXT,
|
||||
client_secret TEXT,
|
||||
issuer_url TEXT,
|
||||
authorization_url TEXT,
|
||||
token_url TEXT,
|
||||
identifier_path TEXT,
|
||||
name_path TEXT,
|
||||
scopes TEXT DEFAULT 'openid email profile',
|
||||
totp_secret TEXT,
|
||||
totp_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
totp_backup_codes TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dotenv from "dotenv";
|
||||
import { promises as fs } from "fs";
|
||||
import { readFileSync } from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
|
||||
@@ -23,18 +24,52 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
} catch {}
|
||||
|
||||
let version = "unknown";
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const packageJsonPath = path.join(
|
||||
path.dirname(__filename),
|
||||
"../../../package.json",
|
||||
);
|
||||
const packageJson = JSON.parse(
|
||||
await fs.readFile(packageJsonPath, "utf-8"),
|
||||
);
|
||||
version = packageJson.version || "unknown";
|
||||
} catch (error) {
|
||||
version = process.env.VERSION || "unknown";
|
||||
|
||||
const versionSources = [
|
||||
() => process.env.VERSION,
|
||||
() => {
|
||||
try {
|
||||
const packageJsonPath = path.join(process.cwd(), "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const packageJsonPath = path.join(
|
||||
path.dirname(__filename),
|
||||
"../../../package.json",
|
||||
);
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
try {
|
||||
const packageJsonPath = path.join("/app", "package.json");
|
||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||
return packageJson.version;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
for (const getVersion of versionSources) {
|
||||
try {
|
||||
const foundVersion = getVersion();
|
||||
if (foundVersion && foundVersion !== "unknown") {
|
||||
version = foundVersion;
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
|
||||
operation: "startup",
|
||||
@@ -50,79 +85,6 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
||||
|
||||
const dbModule = await import("./database/db/index.js");
|
||||
await dbModule.initializeDatabase();
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
const securityIssues: string[] = [];
|
||||
|
||||
if (!process.env.JWT_SECRET) {
|
||||
systemLogger.warn(
|
||||
"JWT_SECRET not set - using auto-generated keys (consider setting for production)",
|
||||
{
|
||||
operation: "security_warning",
|
||||
note: "Auto-generated keys are secure but not persistent across deployments",
|
||||
},
|
||||
);
|
||||
} else if (process.env.JWT_SECRET.length < 64) {
|
||||
securityIssues.push(
|
||||
"JWT_SECRET should be at least 64 characters in production",
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.DATABASE_KEY) {
|
||||
systemLogger.warn(
|
||||
"DATABASE_KEY not set - using auto-generated keys (consider setting for production)",
|
||||
{
|
||||
operation: "security_warning",
|
||||
note: "Auto-generated keys are secure but not persistent across deployments",
|
||||
},
|
||||
);
|
||||
} else if (process.env.DATABASE_KEY.length < 64) {
|
||||
securityIssues.push(
|
||||
"DATABASE_KEY should be at least 64 characters in production",
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.INTERNAL_AUTH_TOKEN) {
|
||||
systemLogger.warn(
|
||||
"INTERNAL_AUTH_TOKEN not set - using auto-generated token (consider setting for production)",
|
||||
{
|
||||
operation: "security_warning",
|
||||
note: "Auto-generated tokens are secure but not persistent across deployments",
|
||||
},
|
||||
);
|
||||
} else if (process.env.INTERNAL_AUTH_TOKEN.length < 32) {
|
||||
securityIssues.push(
|
||||
"INTERNAL_AUTH_TOKEN should be at least 32 characters in production",
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.DB_FILE_ENCRYPTION === "false") {
|
||||
securityIssues.push(
|
||||
"Database file encryption should be enabled in production",
|
||||
);
|
||||
}
|
||||
|
||||
systemLogger.warn(
|
||||
"Production deployment detected - ensure CORS is properly configured",
|
||||
{
|
||||
operation: "security_checks",
|
||||
warning: "Verify frontend domain whitelist",
|
||||
},
|
||||
);
|
||||
|
||||
if (securityIssues.length > 0) {
|
||||
systemLogger.error("SECURITY ISSUES DETECTED IN PRODUCTION:", {
|
||||
operation: "security_checks_failed",
|
||||
issues: securityIssues,
|
||||
});
|
||||
for (const issue of securityIssues) {
|
||||
systemLogger.error(`- ${issue}`, { operation: "security_issue" });
|
||||
}
|
||||
systemLogger.error("Fix these issues before running in production!", {
|
||||
operation: "security_checks_failed",
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const authManager = AuthManager.getInstance();
|
||||
await authManager.initialize();
|
||||
|
||||
@@ -29,28 +29,17 @@ export class AutoSSLSetup {
|
||||
|
||||
try {
|
||||
if (await this.isSSLConfigured()) {
|
||||
await this.logCertificateInfo();
|
||||
await this.setupEnvironmentVariables();
|
||||
return;
|
||||
}
|
||||
|
||||
// In Docker, certificates might be generated by entrypoint script
|
||||
// Check if they exist but weren't detected by isSSLConfigured
|
||||
try {
|
||||
await fs.access(this.CERT_FILE);
|
||||
await fs.access(this.KEY_FILE);
|
||||
|
||||
systemLogger.info("SSL certificates found from entrypoint script", {
|
||||
operation: "ssl_cert_found_entrypoint",
|
||||
cert_path: this.CERT_FILE,
|
||||
key_path: this.KEY_FILE,
|
||||
});
|
||||
|
||||
await this.logCertificateInfo();
|
||||
|
||||
await this.setupEnvironmentVariables();
|
||||
return;
|
||||
} catch {
|
||||
// Certificates don't exist, generate them
|
||||
await this.generateSSLCertificates();
|
||||
await this.setupEnvironmentVariables();
|
||||
}
|
||||
@@ -169,8 +158,6 @@ IP.2 = ::1
|
||||
key_path: this.KEY_FILE,
|
||||
valid_days: 365,
|
||||
});
|
||||
|
||||
await this.logCertificateInfo();
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
@@ -178,49 +165,6 @@ IP.2 = ::1
|
||||
}
|
||||
}
|
||||
|
||||
private static async logCertificateInfo(): Promise<void> {
|
||||
try {
|
||||
const subject = execSync(
|
||||
`openssl x509 -in "${this.CERT_FILE}" -noout -subject`,
|
||||
{ stdio: "pipe" },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
const issuer = execSync(
|
||||
`openssl x509 -in "${this.CERT_FILE}" -noout -issuer`,
|
||||
{ stdio: "pipe" },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
const notAfter = execSync(
|
||||
`openssl x509 -in "${this.CERT_FILE}" -noout -enddate`,
|
||||
{ stdio: "pipe" },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
const notBefore = execSync(
|
||||
`openssl x509 -in "${this.CERT_FILE}" -noout -startdate`,
|
||||
{ stdio: "pipe" },
|
||||
)
|
||||
.toString()
|
||||
.trim();
|
||||
|
||||
systemLogger.info("SSL Certificate Information:", {
|
||||
operation: "ssl_cert_info",
|
||||
subject: subject.replace("subject=", ""),
|
||||
issuer: issuer.replace("issuer=", ""),
|
||||
valid_from: notBefore.replace("notBefore=", ""),
|
||||
valid_until: notAfter.replace("notAfter=", ""),
|
||||
note: "Certificate will auto-renew 30 days before expiration",
|
||||
});
|
||||
} catch (error) {
|
||||
systemLogger.warn("Could not retrieve certificate information", {
|
||||
operation: "ssl_cert_info_error",
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static async setupEnvironmentVariables(): Promise<void> {
|
||||
const certPath = this.CERT_FILE;
|
||||
const keyPath = this.KEY_FILE;
|
||||
|
||||
@@ -1,23 +1,47 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
||||
import { Toaster as Sonner, type ToasterProps, toast } from "sonner";
|
||||
import { useRef } from "react";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
const { theme = "system" } = useTheme();
|
||||
const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null);
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
const originalToast = toast;
|
||||
|
||||
const rateLimitedToast = (message: string, options?: any) => {
|
||||
const now = Date.now();
|
||||
const lastToast = lastToastRef.current;
|
||||
|
||||
if (lastToast && lastToast.text === message && (now - lastToast.timestamp) < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastToastRef.current = { text: message, timestamp: now };
|
||||
return originalToast(message, options);
|
||||
};
|
||||
|
||||
Object.assign(toast, {
|
||||
success: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'success' }),
|
||||
error: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'error' }),
|
||||
warning: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'warning' }),
|
||||
info: (message: string, options?: any) => rateLimitedToast(message, { ...options, type: 'info' }),
|
||||
message: rateLimitedToast,
|
||||
});
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
export { Toaster };
|
||||
180
src/components/ui/version-check-modal.tsx
Normal file
180
src/components/ui/version-check-modal.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { VersionAlert } from "@/components/ui/version-alert.tsx";
|
||||
import { RefreshCw, X } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { checkElectronUpdate, isElectron } from "@/ui/main-axios.ts";
|
||||
|
||||
interface VersionCheckModalProps {
|
||||
onDismiss: () => void;
|
||||
onContinue: () => void;
|
||||
}
|
||||
|
||||
export function VersionCheckModal({ onDismiss, onContinue }: VersionCheckModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||
const [versionChecking, setVersionChecking] = useState(false);
|
||||
const [versionDismissed, setVersionDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
checkForUpdates();
|
||||
} else {
|
||||
onContinue();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
setVersionChecking(true);
|
||||
try {
|
||||
const updateInfo = await checkElectronUpdate();
|
||||
setVersionInfo(updateInfo);
|
||||
} catch (error) {
|
||||
console.error("Failed to check for updates:", error);
|
||||
setVersionInfo({ success: false, error: "Check failed" });
|
||||
} finally {
|
||||
setVersionChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVersionDismiss = () => {
|
||||
setVersionDismissed(true);
|
||||
};
|
||||
|
||||
const handleDownloadUpdate = () => {
|
||||
if (versionInfo?.latest_release?.html_url) {
|
||||
window.open(versionInfo.latest_release.html_url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const handleContinue = () => {
|
||||
onContinue();
|
||||
};
|
||||
|
||||
if (!isElectron()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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="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>
|
||||
<p className="text-center text-muted-foreground">
|
||||
{t("versionCheck.checkingUpdates")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!versionInfo || versionInfo.status === "up_to_date" || 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">
|
||||
<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"
|
||||
>
|
||||
{t("common.continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
<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"
|
||||
>
|
||||
{t("common.continue")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -226,7 +226,10 @@
|
||||
"downloadUpdate": "Download Update",
|
||||
"dismiss": "Dismiss",
|
||||
"checking": "Checking for updates...",
|
||||
"checkUpdates": "Check for Updates"
|
||||
"checkUpdates": "Check for Updates",
|
||||
"checkingUpdates": "Checking for updates...",
|
||||
"refresh": "Refresh",
|
||||
"updateRequired": "Update Required"
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
@@ -1207,7 +1210,7 @@
|
||||
"databaseConnected": "Database connected successfully",
|
||||
"databaseConnectionFailed": "Failed to connect to the database server",
|
||||
"checkServerConnection": "Please check your server connection and try again",
|
||||
"resetCodeSent": "Reset code sent to your email",
|
||||
"resetCodeSent": "Reset code sent to Docker logs",
|
||||
"codeVerified": "Code verified successfully",
|
||||
"passwordResetSuccess": "Password reset successfully",
|
||||
"loginSuccess": "Login successful",
|
||||
|
||||
@@ -224,7 +224,10 @@
|
||||
"downloadUpdate": "下载更新",
|
||||
"dismiss": "忽略",
|
||||
"checking": "正在检查更新...",
|
||||
"checkUpdates": "检查更新"
|
||||
"checkUpdates": "检查更新",
|
||||
"checkingUpdates": "正在检查更新...",
|
||||
"refresh": "刷新",
|
||||
"updateRequired": "需要更新"
|
||||
},
|
||||
"common": {
|
||||
"close": "关闭",
|
||||
@@ -1184,7 +1187,7 @@
|
||||
"databaseConnected": "数据库连接成功",
|
||||
"databaseConnectionFailed": "无法连接到数据库服务器",
|
||||
"checkServerConnection": "请检查您的服务器连接并重试",
|
||||
"resetCodeSent": "重置代码已发送到您的邮箱",
|
||||
"resetCodeSent": "重置代码已发送到 Docker 日志",
|
||||
"codeVerified": "代码验证成功",
|
||||
"passwordResetSuccess": "密码重置成功",
|
||||
"loginSuccess": "登录成功",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { TopNavbar } from "@/ui/Desktop/Navigation/TopNavbar.tsx";
|
||||
import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx";
|
||||
import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx";
|
||||
import { Toaster } from "@/components/ui/sonner.tsx";
|
||||
import { VersionCheckModal } from "@/components/ui/version-check-modal.tsx";
|
||||
import { getUserInfo, getCookie } from "@/ui/main-axios.ts";
|
||||
|
||||
function AppContent() {
|
||||
@@ -22,6 +23,7 @@ function AppContent() {
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [showVersionCheck, setShowVersionCheck] = useState(true);
|
||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||
const { currentTab, tabs } = useTabs();
|
||||
|
||||
@@ -94,6 +96,13 @@ function AppContent() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showVersionCheck && (
|
||||
<VersionCheckModal
|
||||
onDismiss={() => setShowVersionCheck(false)}
|
||||
onContinue={() => setShowVersionCheck(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAuthenticated && !authLoading && (
|
||||
<div>
|
||||
<div
|
||||
|
||||
@@ -3,16 +3,14 @@ import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||
import { VersionAlert } from "@/components/ui/version-alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getServerConfig,
|
||||
saveServerConfig,
|
||||
testServerConnection,
|
||||
checkElectronUpdate,
|
||||
type ServerConfig,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
|
||||
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
|
||||
|
||||
interface ServerConfigProps {
|
||||
onServerConfigured: (serverUrl: string) => void;
|
||||
@@ -33,13 +31,9 @@ export function ServerConfig({
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
"unknown" | "success" | "error"
|
||||
>("unknown");
|
||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
||||
const [versionChecking, setVersionChecking] = useState(false);
|
||||
const [versionDismissed, setVersionDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadServerConfig();
|
||||
checkForUpdates();
|
||||
}, []);
|
||||
|
||||
const loadServerConfig = async () => {
|
||||
@@ -52,29 +46,6 @@ export function ServerConfig({
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const checkForUpdates = async () => {
|
||||
setVersionChecking(true);
|
||||
try {
|
||||
const updateInfo = await checkElectronUpdate();
|
||||
setVersionInfo(updateInfo);
|
||||
} catch (error) {
|
||||
console.error("Failed to check for updates:", error);
|
||||
setVersionInfo({ success: false, error: "Check failed" });
|
||||
} finally {
|
||||
setVersionChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVersionDismiss = () => {
|
||||
setVersionDismissed(true);
|
||||
};
|
||||
|
||||
const handleDownloadUpdate = () => {
|
||||
if (versionInfo?.latest_release?.html_url) {
|
||||
window.open(versionInfo.latest_release.html_url, "_blank");
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async () => {
|
||||
if (!serverUrl.trim()) {
|
||||
setError(t("serverConfig.enterServerUrl"));
|
||||
@@ -224,36 +195,6 @@ export function ServerConfig({
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Version Check Section */}
|
||||
{versionInfo && !versionDismissed && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium">
|
||||
{t("versionCheck.checkUpdates")}
|
||||
</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={checkForUpdates}
|
||||
disabled={versionChecking}
|
||||
className="h-6 px-2"
|
||||
>
|
||||
{versionChecking ? (
|
||||
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<VersionAlert
|
||||
updateInfo={versionInfo}
|
||||
onDismiss={handleVersionDismiss}
|
||||
onDownload={handleDownloadUpdate}
|
||||
showDismiss={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{onCancel && !isFirstTime && (
|
||||
<Button
|
||||
|
||||
@@ -526,8 +526,12 @@ function handleApiError(error: unknown, operation: string): never {
|
||||
`Auth failed: ${method} ${url} - ${message}`,
|
||||
errorContext,
|
||||
);
|
||||
|
||||
const isLoginEndpoint = url?.includes('/users/login');
|
||||
const errorMessage = isLoginEndpoint ? message : "Authentication required. Please log in again.";
|
||||
|
||||
throw new ApiError(
|
||||
"Authentication required. Please log in again.",
|
||||
errorMessage,
|
||||
401,
|
||||
"AUTH_REQUIRED",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user