refactor(runtime): prefer env-backed secrets at runtime
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import type { PrismaClient } from "@capakraken/db";
|
||||
import { resolveSystemSettingsRuntime } from "./system-settings-runtime.js";
|
||||
|
||||
const DEFAULT_ANONYMIZATION_DOMAIN = "superhartmut.de";
|
||||
const DEFAULT_ANONYMIZATION_SEED = "capakraken-superhartmut-global";
|
||||
@@ -639,10 +640,12 @@ export async function getAnonymizationConfig(
|
||||
},
|
||||
});
|
||||
|
||||
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
||||
|
||||
return {
|
||||
enabled: settings?.anonymizationEnabled ?? false,
|
||||
domain: settings?.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
|
||||
seed: settings?.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
|
||||
enabled: runtimeSettings.anonymizationEnabled ?? false,
|
||||
domain: runtimeSettings.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
|
||||
seed: runtimeSettings.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
|
||||
mode: "global",
|
||||
};
|
||||
}
|
||||
@@ -665,10 +668,12 @@ export async function getAnonymizationDirectory(
|
||||
},
|
||||
});
|
||||
|
||||
const runtimeSettings = resolveSystemSettingsRuntime(settings);
|
||||
|
||||
const config: AnonymizationConfig = {
|
||||
enabled: settings?.anonymizationEnabled ?? false,
|
||||
domain: settings?.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
|
||||
seed: settings?.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
|
||||
enabled: runtimeSettings.anonymizationEnabled ?? false,
|
||||
domain: runtimeSettings.anonymizationDomain?.trim() || DEFAULT_ANONYMIZATION_DOMAIN,
|
||||
seed: runtimeSettings.anonymizationSeed?.trim() || DEFAULT_ANONYMIZATION_SEED,
|
||||
mode: "global",
|
||||
};
|
||||
|
||||
@@ -680,7 +685,7 @@ export async function getAnonymizationDirectory(
|
||||
const usedSlugs = new Set<string>();
|
||||
const byResourceId = new Map<string, ResourceAlias>();
|
||||
const byAliasEid = new Map<string, string>();
|
||||
const storedAliases = parseStoredAliases(settings?.anonymizationAliases);
|
||||
const storedAliases = parseStoredAliases(runtimeSettings.anonymizationAliases);
|
||||
let aliasesChanged = false;
|
||||
|
||||
for (const [resourceId, storedAlias] of Object.entries(storedAliases)) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
*/
|
||||
import nodemailer from "nodemailer";
|
||||
import { prisma as db } from "@capakraken/db";
|
||||
import { logger } from "./logger.js";
|
||||
import { resolveSystemSettingsRuntime } from "./system-settings-runtime.js";
|
||||
|
||||
interface EmailPayload {
|
||||
to: string | string[];
|
||||
@@ -12,9 +14,43 @@ interface EmailPayload {
|
||||
html?: string;
|
||||
}
|
||||
|
||||
function sanitizeSmtpDiagnostic(err: unknown): string {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return message
|
||||
.replace(/https?:\/\/[^\s)\]}]+/gi, "<redacted-url>")
|
||||
.replace(/\b[^\s@]+@[^\s@]+\.[^\s@]+\b/g, "<redacted-email>")
|
||||
.replace(/\b(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}\b/g, "<redacted-host>")
|
||||
.replace(/^Error:\s*/, "")
|
||||
.slice(0, 300);
|
||||
}
|
||||
|
||||
export function parseSmtpError(err: unknown): string {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
const lower = message.toLowerCase();
|
||||
|
||||
if (lower.includes("auth") || lower.includes("invalid login") || lower.includes("535")) {
|
||||
return "SMTP authentication failed — check username and password.";
|
||||
}
|
||||
if (lower.includes("certificate") || lower.includes("tls") || lower.includes("ssl")) {
|
||||
return "SMTP TLS negotiation failed — verify port and TLS settings.";
|
||||
}
|
||||
if (
|
||||
lower.includes("econnrefused")
|
||||
|| lower.includes("enotfound")
|
||||
|| lower.includes("etimedout")
|
||||
|| lower.includes("timeout")
|
||||
) {
|
||||
return "Cannot reach the SMTP server — check host, port, and network access.";
|
||||
}
|
||||
|
||||
return "SMTP connection failed — review host, port, TLS, and credentials.";
|
||||
}
|
||||
|
||||
async function getSmtpConfig() {
|
||||
const settings = await db.systemSettings.findUnique({ where: { id: "singleton" } });
|
||||
if (!settings?.smtpHost) return null;
|
||||
const settings = resolveSystemSettingsRuntime(
|
||||
await db.systemSettings.findUnique({ where: { id: "singleton" } }),
|
||||
);
|
||||
if (!settings.smtpHost) return null;
|
||||
return {
|
||||
host: settings.smtpHost,
|
||||
port: settings.smtpPort ?? 587,
|
||||
@@ -53,7 +89,7 @@ export async function sendEmail(payload: EmailPayload): Promise<boolean> {
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error("[email] Failed to send email:", err);
|
||||
logger.warn({ diagnostic: sanitizeSmtpDiagnostic(err) }, "Email send failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -76,6 +112,7 @@ export async function testSmtpConnection(): Promise<{ ok: boolean; error?: strin
|
||||
await transporter.verify();
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
logger.warn({ diagnostic: sanitizeSmtpDiagnostic(err) }, "SMTP connection test failed");
|
||||
return { ok: false, error: parseSmtpError(err) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
type RuntimeAwareSystemSettings = {
|
||||
aiProvider?: string | null;
|
||||
azureOpenAiApiKey?: string | null;
|
||||
azureDalleApiKey?: string | null;
|
||||
geminiApiKey?: string | null;
|
||||
smtpPassword?: string | null;
|
||||
anonymizationSeed?: string | null;
|
||||
};
|
||||
|
||||
function readEnvOverride(...names: string[]): string | null {
|
||||
for (const name of names) {
|
||||
const value = process.env[name]?.trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePrimaryAiApiKey(provider: string | null | undefined): string | null {
|
||||
if (provider === "azure") {
|
||||
return readEnvOverride("AZURE_OPENAI_API_KEY", "OPENAI_API_KEY");
|
||||
}
|
||||
|
||||
return readEnvOverride("OPENAI_API_KEY", "AZURE_OPENAI_API_KEY");
|
||||
}
|
||||
|
||||
export function resolveSystemSettingsRuntime<T extends RuntimeAwareSystemSettings>(
|
||||
settings: T | null | undefined,
|
||||
): T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "anonymizationSeed">> {
|
||||
const resolved = { ...(settings ?? {}) } as T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "anonymizationSeed">>;
|
||||
|
||||
resolved.azureOpenAiApiKey = resolvePrimaryAiApiKey(resolved.aiProvider) ?? settings?.azureOpenAiApiKey ?? null;
|
||||
resolved.azureDalleApiKey = readEnvOverride("AZURE_DALLE_API_KEY") ?? settings?.azureDalleApiKey ?? null;
|
||||
resolved.geminiApiKey = readEnvOverride("GEMINI_API_KEY") ?? settings?.geminiApiKey ?? null;
|
||||
resolved.smtpPassword = readEnvOverride("SMTP_PASSWORD") ?? settings?.smtpPassword ?? null;
|
||||
resolved.anonymizationSeed = readEnvOverride("ANONYMIZATION_SEED") ?? settings?.anonymizationSeed ?? null;
|
||||
|
||||
return resolved;
|
||||
}
|
||||
Reference in New Issue
Block a user