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_platform=linux
|
||||||
ENV npm_config_target_arch=x64
|
ENV npm_config_target_arch=x64
|
||||||
ENV npm_config_target_libc=glibc
|
ENV npm_config_target_libc=glibc
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
||||||
|
|
||||||
RUN npm ci --force --ignore-scripts && \
|
RUN npm ci --force --ignore-scripts && \
|
||||||
npm cache clean --force
|
npm cache clean --force
|
||||||
@@ -20,11 +19,7 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm run build:types
|
|
||||||
|
|
||||||
# Stage 3: Build backend TypeScript
|
# Stage 3: Build backend TypeScript
|
||||||
FROM deps AS backend-builder
|
FROM deps AS backend-builder
|
||||||
@@ -35,8 +30,6 @@ COPY . .
|
|||||||
ENV npm_config_target_platform=linux
|
ENV npm_config_target_platform=linux
|
||||||
ENV npm_config_target_arch=x64
|
ENV npm_config_target_arch=x64
|
||||||
ENV npm_config_target_libc=glibc
|
ENV npm_config_target_libc=glibc
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
||||||
ENV NODE_ENV=production
|
|
||||||
|
|
||||||
RUN npm rebuild better-sqlite3 --force
|
RUN npm rebuild better-sqlite3 --force
|
||||||
|
|
||||||
@@ -46,14 +39,13 @@ RUN npm run build:backend
|
|||||||
FROM node:24-alpine AS production-deps
|
FROM node:24-alpine AS production-deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apk add --no-cache python3 make g++
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
ENV npm_config_target_platform=linux
|
ENV npm_config_target_platform=linux
|
||||||
ENV npm_config_target_arch=x64
|
ENV npm_config_target_arch=x64
|
||||||
ENV npm_config_target_libc=glibc
|
ENV npm_config_target_libc=glibc
|
||||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
|
||||||
|
|
||||||
RUN npm ci --only=production --ignore-scripts --force && \
|
RUN npm ci --only=production --ignore-scripts --force && \
|
||||||
npm rebuild better-sqlite3 bcryptjs --force && \
|
npm rebuild better-sqlite3 bcryptjs --force && \
|
||||||
@@ -90,4 +82,4 @@ EXPOSE ${PORT} 30001 30002 30003 30004 30005
|
|||||||
|
|
||||||
COPY docker/entrypoint.sh /entrypoint.sh
|
COPY docker/entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /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}
|
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
|
if [ -f "/app/data/ssl/termix.crt" ] && [ -f "/app/data/ssl/termix.key" ]; then
|
||||||
echo "SSL certificates found, checking validity..."
|
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
|
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"
|
echo "SSL certificates are valid and will be reused for domain: $DOMAIN"
|
||||||
else
|
else
|
||||||
@@ -47,7 +45,6 @@ if [ "$ENABLE_SSL" = "true" ]; then
|
|||||||
echo "SSL certificates not found, will generate new ones..."
|
echo "SSL certificates not found, will generate new ones..."
|
||||||
fi
|
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
|
if [ ! -f "/app/data/ssl/termix.crt" ] || [ ! -f "/app/data/ssl/termix.key" ]; then
|
||||||
echo "Generating SSL certificates for domain: $DOMAIN"
|
echo "Generating SSL certificates for domain: $DOMAIN"
|
||||||
|
|
||||||
@@ -101,6 +98,18 @@ echo "Starting backend services..."
|
|||||||
cd /app
|
cd /app
|
||||||
export NODE_ENV=production
|
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
|
if command -v su-exec > /dev/null 2>&1; then
|
||||||
su-exec node node dist/backend/backend/starter.js
|
su-exec node node dist/backend/backend/starter.js
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -126,26 +126,31 @@ async function fetchGitHubAPI(endpoint, cacheKey) {
|
|||||||
const isHttps = urlObj.protocol === "https:";
|
const isHttps = urlObj.protocol === "https:";
|
||||||
const client = isHttps ? https : http;
|
const client = isHttps ? https : http;
|
||||||
|
|
||||||
const req = client.request(
|
const requestOptions = {
|
||||||
url,
|
method: options.method || "GET",
|
||||||
{
|
headers: options.headers || {},
|
||||||
method: options.method || "GET",
|
timeout: options.timeout || 10000,
|
||||||
headers: options.headers || {},
|
};
|
||||||
timeout: options.timeout || 10000,
|
|
||||||
},
|
if (isHttps) {
|
||||||
(res) => {
|
requestOptions.rejectUnauthorized = false;
|
||||||
let data = "";
|
requestOptions.agent = new https.Agent({
|
||||||
res.on("data", (chunk) => (data += chunk));
|
rejectUnauthorized: false,
|
||||||
res.on("end", () => {
|
});
|
||||||
resolve({
|
}
|
||||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
||||||
status: res.statusCode,
|
const req = client.request(url, requestOptions, (res) => {
|
||||||
text: () => Promise.resolve(data),
|
let data = "";
|
||||||
json: () => Promise.resolve(JSON.parse(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("error", reject);
|
||||||
req.on("timeout", () => {
|
req.on("timeout", () => {
|
||||||
@@ -295,26 +300,31 @@ ipcMain.handle("test-server-connection", async (event, serverUrl) => {
|
|||||||
const isHttps = urlObj.protocol === "https:";
|
const isHttps = urlObj.protocol === "https:";
|
||||||
const client = isHttps ? https : http;
|
const client = isHttps ? https : http;
|
||||||
|
|
||||||
const req = client.request(
|
const requestOptions = {
|
||||||
url,
|
method: options.method || "GET",
|
||||||
{
|
headers: options.headers || {},
|
||||||
method: options.method || "GET",
|
timeout: options.timeout || 5000,
|
||||||
headers: options.headers || {},
|
};
|
||||||
timeout: options.timeout || 5000,
|
|
||||||
},
|
if (isHttps) {
|
||||||
(res) => {
|
requestOptions.rejectUnauthorized = false;
|
||||||
let data = "";
|
requestOptions.agent = new https.Agent({
|
||||||
res.on("data", (chunk) => (data += chunk));
|
rejectUnauthorized: false,
|
||||||
res.on("end", () => {
|
});
|
||||||
resolve({
|
}
|
||||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
||||||
status: res.statusCode,
|
const req = client.request(url, requestOptions, (res) => {
|
||||||
text: () => Promise.resolve(data),
|
let data = "";
|
||||||
json: () => Promise.resolve(JSON.parse(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("error", reject);
|
||||||
req.on("timeout", () => {
|
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",
|
"clean": "npx prettier . --write",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:https": "cross-env VITE_HTTPS=true vite",
|
"dev:https": "cross-env VITE_HTTPS=true vite",
|
||||||
"build": "vite build",
|
"build": "vite build && tsc -p tsconfig.node.json",
|
||||||
"build:types": "tsc -p tsconfig.node.json",
|
|
||||||
"build:full": "npm run build && npm run build:types",
|
|
||||||
"build:backend": "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",
|
"dev:backend": "tsc -p tsconfig.node.json && node ./dist/backend/backend/starter.js",
|
||||||
"start": "npm run build:backend && 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",
|
"typescript-eslint": "^8.40.0",
|
||||||
"vite": "^7.1.5"
|
"vite": "^7.1.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,14 +218,46 @@ app.get("/version", authenticateJWT, async (req, res) => {
|
|||||||
let localVersion = process.env.VERSION;
|
let localVersion = process.env.VERSION;
|
||||||
|
|
||||||
if (!localVersion) {
|
if (!localVersion) {
|
||||||
try {
|
const versionSources = [
|
||||||
const packagePath = path.resolve(process.cwd(), "package.json");
|
() => {
|
||||||
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
try {
|
||||||
localVersion = packageJson.version;
|
const packagePath = path.resolve(process.cwd(), "package.json");
|
||||||
} catch (error) {
|
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
||||||
databaseLogger.error("Failed to read version from package.json", error, {
|
return packageJson.version;
|
||||||
operation: "version_check",
|
} 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,
|
password_hash TEXT NOT NULL,
|
||||||
is_admin INTEGER NOT NULL DEFAULT 0,
|
is_admin INTEGER NOT NULL DEFAULT 0,
|
||||||
is_oidc INTEGER NOT NULL DEFAULT 0,
|
is_oidc INTEGER NOT NULL DEFAULT 0,
|
||||||
client_id TEXT NOT NULL,
|
oidc_identifier TEXT,
|
||||||
client_secret TEXT NOT NULL,
|
client_id TEXT,
|
||||||
issuer_url TEXT NOT NULL,
|
client_secret TEXT,
|
||||||
authorization_url TEXT NOT NULL,
|
issuer_url TEXT,
|
||||||
token_url TEXT NOT NULL,
|
authorization_url TEXT,
|
||||||
redirect_uri TEXT,
|
token_url TEXT,
|
||||||
identifier_path TEXT NOT NULL,
|
identifier_path TEXT,
|
||||||
name_path TEXT NOT NULL,
|
name_path TEXT,
|
||||||
scopes TEXT NOT NULL
|
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 (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from "fs";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
|
import { AutoSSLSetup } from "./utils/auto-ssl-setup.js";
|
||||||
@@ -23,18 +24,52 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
let version = "unknown";
|
let version = "unknown";
|
||||||
try {
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const versionSources = [
|
||||||
const packageJsonPath = path.join(
|
() => process.env.VERSION,
|
||||||
path.dirname(__filename),
|
() => {
|
||||||
"../../../package.json",
|
try {
|
||||||
);
|
const packageJsonPath = path.join(process.cwd(), "package.json");
|
||||||
const packageJson = JSON.parse(
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
||||||
await fs.readFile(packageJsonPath, "utf-8"),
|
return packageJson.version;
|
||||||
);
|
} catch {
|
||||||
version = packageJson.version || "unknown";
|
return null;
|
||||||
} catch (error) {
|
}
|
||||||
version = process.env.VERSION || "unknown";
|
},
|
||||||
|
() => {
|
||||||
|
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}`, {
|
versionLogger.info(`Termix Backend starting - Version: ${version}`, {
|
||||||
operation: "startup",
|
operation: "startup",
|
||||||
@@ -50,79 +85,6 @@ import { systemLogger, versionLogger } from "./utils/logger.js";
|
|||||||
|
|
||||||
const dbModule = await import("./database/db/index.js");
|
const dbModule = await import("./database/db/index.js");
|
||||||
await dbModule.initializeDatabase();
|
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();
|
const authManager = AuthManager.getInstance();
|
||||||
await authManager.initialize();
|
await authManager.initialize();
|
||||||
|
|||||||
@@ -29,28 +29,17 @@ export class AutoSSLSetup {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (await this.isSSLConfigured()) {
|
if (await this.isSSLConfigured()) {
|
||||||
await this.logCertificateInfo();
|
|
||||||
await this.setupEnvironmentVariables();
|
await this.setupEnvironmentVariables();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// In Docker, certificates might be generated by entrypoint script
|
|
||||||
// Check if they exist but weren't detected by isSSLConfigured
|
|
||||||
try {
|
try {
|
||||||
await fs.access(this.CERT_FILE);
|
await fs.access(this.CERT_FILE);
|
||||||
await fs.access(this.KEY_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();
|
await this.setupEnvironmentVariables();
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
// Certificates don't exist, generate them
|
|
||||||
await this.generateSSLCertificates();
|
await this.generateSSLCertificates();
|
||||||
await this.setupEnvironmentVariables();
|
await this.setupEnvironmentVariables();
|
||||||
}
|
}
|
||||||
@@ -169,8 +158,6 @@ IP.2 = ::1
|
|||||||
key_path: this.KEY_FILE,
|
key_path: this.KEY_FILE,
|
||||||
valid_days: 365,
|
valid_days: 365,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.logCertificateInfo();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`SSL certificate generation failed: ${error instanceof Error ? error.message : "Unknown 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> {
|
private static async setupEnvironmentVariables(): Promise<void> {
|
||||||
const certPath = this.CERT_FILE;
|
const certPath = this.CERT_FILE;
|
||||||
const keyPath = this.KEY_FILE;
|
const keyPath = this.KEY_FILE;
|
||||||
|
|||||||
@@ -1,23 +1,47 @@
|
|||||||
import { useTheme } from "next-themes";
|
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 Toaster = ({ ...props }: ToasterProps) => {
|
||||||
const { theme = "system" } = useTheme();
|
const { theme = "system" } = useTheme();
|
||||||
|
const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null);
|
||||||
|
|
||||||
return (
|
const originalToast = toast;
|
||||||
<Sonner
|
|
||||||
theme={theme as ToasterProps["theme"]}
|
const rateLimitedToast = (message: string, options?: any) => {
|
||||||
className="toaster group"
|
const now = Date.now();
|
||||||
style={
|
const lastToast = lastToastRef.current;
|
||||||
{
|
|
||||||
"--normal-bg": "var(--popover)",
|
if (lastToast && lastToast.text === message && (now - lastToast.timestamp) < 1000) {
|
||||||
"--normal-text": "var(--popover-foreground)",
|
return;
|
||||||
"--normal-border": "var(--border)",
|
}
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
lastToastRef.current = { text: message, timestamp: now };
|
||||||
{...props}
|
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",
|
"downloadUpdate": "Download Update",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
"checking": "Checking for updates...",
|
"checking": "Checking for updates...",
|
||||||
"checkUpdates": "Check for Updates"
|
"checkUpdates": "Check for Updates",
|
||||||
|
"checkingUpdates": "Checking for updates...",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"updateRequired": "Update Required"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
@@ -1207,7 +1210,7 @@
|
|||||||
"databaseConnected": "Database connected successfully",
|
"databaseConnected": "Database connected successfully",
|
||||||
"databaseConnectionFailed": "Failed to connect to the database server",
|
"databaseConnectionFailed": "Failed to connect to the database server",
|
||||||
"checkServerConnection": "Please check your server connection and try again",
|
"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",
|
"codeVerified": "Code verified successfully",
|
||||||
"passwordResetSuccess": "Password reset successfully",
|
"passwordResetSuccess": "Password reset successfully",
|
||||||
"loginSuccess": "Login successful",
|
"loginSuccess": "Login successful",
|
||||||
|
|||||||
@@ -224,7 +224,10 @@
|
|||||||
"downloadUpdate": "下载更新",
|
"downloadUpdate": "下载更新",
|
||||||
"dismiss": "忽略",
|
"dismiss": "忽略",
|
||||||
"checking": "正在检查更新...",
|
"checking": "正在检查更新...",
|
||||||
"checkUpdates": "检查更新"
|
"checkUpdates": "检查更新",
|
||||||
|
"checkingUpdates": "正在检查更新...",
|
||||||
|
"refresh": "刷新",
|
||||||
|
"updateRequired": "需要更新"
|
||||||
},
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
@@ -1184,7 +1187,7 @@
|
|||||||
"databaseConnected": "数据库连接成功",
|
"databaseConnected": "数据库连接成功",
|
||||||
"databaseConnectionFailed": "无法连接到数据库服务器",
|
"databaseConnectionFailed": "无法连接到数据库服务器",
|
||||||
"checkServerConnection": "请检查您的服务器连接并重试",
|
"checkServerConnection": "请检查您的服务器连接并重试",
|
||||||
"resetCodeSent": "重置代码已发送到您的邮箱",
|
"resetCodeSent": "重置代码已发送到 Docker 日志",
|
||||||
"codeVerified": "代码验证成功",
|
"codeVerified": "代码验证成功",
|
||||||
"passwordResetSuccess": "密码重置成功",
|
"passwordResetSuccess": "密码重置成功",
|
||||||
"loginSuccess": "登录成功",
|
"loginSuccess": "登录成功",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { TopNavbar } from "@/ui/Desktop/Navigation/TopNavbar.tsx";
|
|||||||
import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx";
|
import { AdminSettings } from "@/ui/Desktop/Admin/AdminSettings.tsx";
|
||||||
import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx";
|
import { UserProfile } from "@/ui/Desktop/User/UserProfile.tsx";
|
||||||
import { Toaster } from "@/components/ui/sonner.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";
|
import { getUserInfo, getCookie } from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
function AppContent() {
|
function AppContent() {
|
||||||
@@ -22,6 +23,7 @@ function AppContent() {
|
|||||||
const [username, setUsername] = useState<string | null>(null);
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
const [isAdmin, setIsAdmin] = useState(false);
|
const [isAdmin, setIsAdmin] = useState(false);
|
||||||
const [authLoading, setAuthLoading] = useState(true);
|
const [authLoading, setAuthLoading] = useState(true);
|
||||||
|
const [showVersionCheck, setShowVersionCheck] = useState(true);
|
||||||
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
const [isTopbarOpen, setIsTopbarOpen] = useState<boolean>(true);
|
||||||
const { currentTab, tabs } = useTabs();
|
const { currentTab, tabs } = useTabs();
|
||||||
|
|
||||||
@@ -94,6 +96,13 @@ function AppContent() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{showVersionCheck && (
|
||||||
|
<VersionCheckModal
|
||||||
|
onDismiss={() => setShowVersionCheck(false)}
|
||||||
|
onContinue={() => setShowVersionCheck(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isAuthenticated && !authLoading && (
|
{!isAuthenticated && !authLoading && (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ import { Button } from "@/components/ui/button.tsx";
|
|||||||
import { Input } from "@/components/ui/input.tsx";
|
import { Input } from "@/components/ui/input.tsx";
|
||||||
import { Label } from "@/components/ui/label.tsx";
|
import { Label } from "@/components/ui/label.tsx";
|
||||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||||
import { VersionAlert } from "@/components/ui/version-alert.tsx";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
getServerConfig,
|
getServerConfig,
|
||||||
saveServerConfig,
|
saveServerConfig,
|
||||||
testServerConnection,
|
testServerConnection,
|
||||||
checkElectronUpdate,
|
|
||||||
type ServerConfig,
|
type ServerConfig,
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
|
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
|
||||||
|
|
||||||
interface ServerConfigProps {
|
interface ServerConfigProps {
|
||||||
onServerConfigured: (serverUrl: string) => void;
|
onServerConfigured: (serverUrl: string) => void;
|
||||||
@@ -33,13 +31,9 @@ export function ServerConfig({
|
|||||||
const [connectionStatus, setConnectionStatus] = useState<
|
const [connectionStatus, setConnectionStatus] = useState<
|
||||||
"unknown" | "success" | "error"
|
"unknown" | "success" | "error"
|
||||||
>("unknown");
|
>("unknown");
|
||||||
const [versionInfo, setVersionInfo] = useState<any>(null);
|
|
||||||
const [versionChecking, setVersionChecking] = useState(false);
|
|
||||||
const [versionDismissed, setVersionDismissed] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServerConfig();
|
loadServerConfig();
|
||||||
checkForUpdates();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadServerConfig = async () => {
|
const loadServerConfig = async () => {
|
||||||
@@ -52,29 +46,6 @@ export function ServerConfig({
|
|||||||
} catch (error) {}
|
} 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 () => {
|
const handleTestConnection = async () => {
|
||||||
if (!serverUrl.trim()) {
|
if (!serverUrl.trim()) {
|
||||||
setError(t("serverConfig.enterServerUrl"));
|
setError(t("serverConfig.enterServerUrl"));
|
||||||
@@ -224,36 +195,6 @@ export function ServerConfig({
|
|||||||
</Alert>
|
</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">
|
<div className="flex space-x-2">
|
||||||
{onCancel && !isFirstTime && (
|
{onCancel && !isFirstTime && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -526,8 +526,12 @@ function handleApiError(error: unknown, operation: string): never {
|
|||||||
`Auth failed: ${method} ${url} - ${message}`,
|
`Auth failed: ${method} ${url} - ${message}`,
|
||||||
errorContext,
|
errorContext,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoginEndpoint = url?.includes('/users/login');
|
||||||
|
const errorMessage = isLoginEndpoint ? message : "Authentication required. Please log in again.";
|
||||||
|
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
"Authentication required. Please log in again.",
|
errorMessage,
|
||||||
401,
|
401,
|
||||||
"AUTH_REQUIRED",
|
"AUTH_REQUIRED",
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user