Add server variables
All checks were successful
Queue Release Build / prepare (push) Successful in 21s
Deploy Web Apps / deploy (push) Successful in 15m14s
Queue Release Build / build-linux (push) Successful in 22m12s
Queue Release Build / build-windows (push) Successful in 23m20s
Queue Release Build / finalize (push) Successful in 2m12s

This commit is contained in:
2026-03-15 16:12:21 +01:00
parent 150c45c31a
commit f8fd78d21a
4 changed files with 152 additions and 22 deletions

View File

@@ -1,4 +1,5 @@
# Toggle SSL for local development (true/false)
# When true: ng serve uses --ssl, Express API uses HTTPS, Electron loads https://
# When false: plain HTTP everywhere (only works on localhost)
# Overrides server/data/variables.json for local development only
SSL=true

View File

@@ -17,7 +17,7 @@ Desktop chat app with three parts:
Root `.env`:
- `SSL=true` uses HTTPS for Angular, the server, and Electron dev mode
- `PORT=3001` changes the server port
- `PORT=3001` changes the server port in local development and overrides the server app setting
If `SSL=true`, run `./generate-cert.sh` once.
@@ -25,6 +25,10 @@ Server files:
- `server/data/variables.json` holds `klipyApiKey`
- `server/data/variables.json` also holds `releaseManifestUrl` for desktop auto updates
- `server/data/variables.json` can now also hold optional `serverHost` (an IP address or hostname to bind to)
- `server/data/variables.json` can now also hold `serverProtocol` (`http` or `https`)
- `server/data/variables.json` can now also hold `serverPort` (1-65535)
- When `serverProtocol` is `https`, the certificate must match the configured `serverHost` or IP
## Main commands

View File

@@ -2,13 +2,20 @@ import fs from 'fs';
import path from 'path';
import { resolveRuntimePath } from '../runtime-paths';
export type ServerHttpProtocol = 'http' | 'https';
export interface ServerVariablesConfig {
klipyApiKey: string;
releaseManifestUrl: string;
serverPort: number;
serverProtocol: ServerHttpProtocol;
serverHost: string;
}
const DATA_DIR = resolveRuntimePath('data');
const VARIABLES_FILE = path.join(DATA_DIR, 'variables.json');
const DEFAULT_SERVER_PORT = 3001;
const DEFAULT_SERVER_PROTOCOL: ServerHttpProtocol = 'http';
function normalizeKlipyApiKey(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
@@ -18,6 +25,51 @@ function normalizeReleaseManifestUrl(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeServerHost(value: unknown): string {
return typeof value === 'string' ? value.trim() : '';
}
function normalizeServerProtocol(
value: unknown,
fallback: ServerHttpProtocol = DEFAULT_SERVER_PROTOCOL
): ServerHttpProtocol {
if (typeof value === 'boolean') {
return value ? 'https' : 'http';
}
if (typeof value !== 'string') {
return fallback;
}
const normalized = value.trim().toLowerCase();
if (normalized === 'https' || normalized === 'true') {
return 'https';
}
if (normalized === 'http' || normalized === 'false') {
return 'http';
}
return fallback;
}
function normalizeServerPort(value: unknown, fallback = DEFAULT_SERVER_PORT): number {
const parsed = typeof value === 'number'
? value
: typeof value === 'string'
? Number.parseInt(value.trim(), 10)
: Number.NaN;
return Number.isInteger(parsed) && parsed >= 1 && parsed <= 65535
? parsed
: fallback;
}
function hasEnvironmentOverride(value: string | undefined): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
function readRawVariables(): { rawContents: string; parsed: Record<string, unknown> } {
if (!fs.existsSync(VARIABLES_FILE)) {
return { rawContents: '', parsed: {} };
@@ -52,10 +104,14 @@ export function ensureVariablesConfig(): ServerVariablesConfig {
}
const { rawContents, parsed } = readRawVariables();
const { serverIpAddress: legacyServerIpAddress, ...remainingParsed } = parsed;
const normalized = {
...parsed,
klipyApiKey: normalizeKlipyApiKey(parsed.klipyApiKey),
releaseManifestUrl: normalizeReleaseManifestUrl(parsed.releaseManifestUrl)
...remainingParsed,
klipyApiKey: normalizeKlipyApiKey(remainingParsed.klipyApiKey),
releaseManifestUrl: normalizeReleaseManifestUrl(remainingParsed.releaseManifestUrl),
serverPort: normalizeServerPort(remainingParsed.serverPort),
serverProtocol: normalizeServerProtocol(remainingParsed.serverProtocol),
serverHost: normalizeServerHost(remainingParsed.serverHost ?? legacyServerIpAddress)
};
const nextContents = JSON.stringify(normalized, null, 2) + '\n';
@@ -65,7 +121,10 @@ export function ensureVariablesConfig(): ServerVariablesConfig {
return {
klipyApiKey: normalized.klipyApiKey,
releaseManifestUrl: normalized.releaseManifestUrl
releaseManifestUrl: normalized.releaseManifestUrl,
serverPort: normalized.serverPort,
serverProtocol: normalized.serverProtocol,
serverHost: normalized.serverHost
};
}
@@ -84,3 +143,29 @@ export function hasKlipyApiKey(): boolean {
export function getReleaseManifestUrl(): string {
return getVariablesConfig().releaseManifestUrl;
}
export function getServerProtocol(): ServerHttpProtocol {
if (hasEnvironmentOverride(process.env.SSL)) {
return normalizeServerProtocol(process.env.SSL);
}
return getVariablesConfig().serverProtocol;
}
export function getServerPort(): number {
if (hasEnvironmentOverride(process.env.PORT)) {
return normalizeServerPort(process.env.PORT);
}
return getVariablesConfig().serverPort;
}
export function getServerHost(): string | undefined {
const serverHost = getVariablesConfig().serverHost;
return serverHost || undefined;
}
export function isHttpsServerEnabled(): boolean {
return getServerProtocol() === 'https';
}

View File

@@ -14,23 +14,39 @@ import { deleteStaleJoinRequests } from './cqrs';
import { createApp } from './app';
import {
ensureVariablesConfig,
getServerHost,
getVariablesConfigPath,
hasKlipyApiKey
getServerPort,
getServerProtocol,
ServerHttpProtocol
} from './config/variables';
import { setupWebSocket } from './websocket';
const USE_SSL = (process.env.SSL ?? 'false').toLowerCase() === 'true';
const PORT = process.env.PORT || 3001;
function formatHostForUrl(host: string): string {
if (host.startsWith('[') || !host.includes(':')) {
return host;
}
function buildServer(app: ReturnType<typeof createApp>) {
if (USE_SSL) {
return `[${host}]`;
}
function getDisplayHost(serverHost: string | undefined): string {
if (!serverHost || serverHost === '0.0.0.0' || serverHost === '::') {
return 'localhost';
}
return serverHost;
}
function buildServer(app: ReturnType<typeof createApp>, serverProtocol: ServerHttpProtocol) {
if (serverProtocol === 'https') {
const certDir = resolveCertificateDirectory();
const certFile = path.join(certDir, 'localhost.crt');
const keyFile = path.join(certDir, 'localhost.key');
if (!fs.existsSync(certFile) || !fs.existsSync(keyFile)) {
console.error(`SSL=true but certs not found in ${certDir}`);
console.error('Run ./generate-cert.sh first.');
console.error(`HTTPS is enabled but certs were not found in ${certDir}`);
console.error('Add localhost.crt and localhost.key there, or switch serverProtocol to "http".');
process.exit(1);
}
@@ -44,17 +60,31 @@ function buildServer(app: ReturnType<typeof createApp>) {
}
async function bootstrap(): Promise<void> {
ensureVariablesConfig();
const variablesConfig = ensureVariablesConfig();
const serverProtocol = getServerProtocol();
const serverPort = getServerPort();
const serverHost = getServerHost();
const bindHostLabel = serverHost || 'default interface';
console.log('[Config] Variables loaded from:', getVariablesConfigPath());
if (!hasKlipyApiKey()) {
if (
variablesConfig.serverProtocol !== serverProtocol
|| variablesConfig.serverPort !== serverPort
) {
console.log(`[Config] Server runtime override active: protocol=${serverProtocol}, host=${bindHostLabel}, port=${serverPort}`);
} else {
console.log(`[Config] Server runtime config: protocol=${serverProtocol}, host=${bindHostLabel}, port=${serverPort}`);
}
if (!variablesConfig.klipyApiKey) {
console.log('[KLIPY] API key not configured. GIF search is disabled.');
}
await initDatabase();
const app = createApp();
const server = buildServer(app);
const server = buildServer(app, serverProtocol);
setupWebSocket(server);
@@ -64,14 +94,24 @@ async function bootstrap(): Promise<void> {
.catch(err => console.error('Failed to clean up stale join requests:', err));
}, 60 * 1000);
server.listen(PORT, () => {
const proto = USE_SSL ? 'https' : 'http';
const wsProto = USE_SSL ? 'wss' : 'ws';
const onListening = () => {
const displayHost = formatHostForUrl(getDisplayHost(serverHost));
const wsProto = serverProtocol === 'https' ? 'wss' : 'ws';
console.log(`MetoYou signaling server running on port ${PORT} (SSL=${USE_SSL})`);
console.log(` REST API: ${proto}://localhost:${PORT}/api`);
console.log(` WebSocket: ${wsProto}://localhost:${PORT}`);
});
console.log(`MetoYou signaling server running on port ${serverPort} (${serverProtocol.toUpperCase()}, bind host=${bindHostLabel})`);
console.log(` REST API: ${serverProtocol}://${displayHost}:${serverPort}/api`);
console.log(` WebSocket: ${wsProto}://${displayHost}:${serverPort}`);
if (serverProtocol === 'https' && serverHost && !['localhost', '127.0.0.1', '::1'].includes(serverHost)) {
console.warn('[Config] HTTPS certificates must match the configured serverHost/server IP.');
}
};
if (serverHost) {
server.listen(serverPort, serverHost, onListening);
} else {
server.listen(serverPort, onListening);
}
}
bootstrap().catch((err) => {