fceceeee4b
- SMTP: SMTP_HOST/PORT/USER/FROM/TLS now all have ENV override support (previously only SMTP_PASSWORD was env-aware). ENV takes priority over DB. - docker-compose.yml: forward all SMTP_* env vars to app container + add Mailhog service (ports 1025 SMTP / 8025 HTTP, always available in dev) - Password reset: PasswordResetToken Prisma model + authRouter with requestPasswordReset (timing-safe, no email enumeration) + resetPassword - UI: /auth/forgot-password, /auth/reset-password/[token] pages + "Forgot password?" link on sign-in page - E2E: Mailhog helpers (getLatestEmailTo, clearMailhog, extractUrlFromEmail) + invite-flow.spec.ts + password-reset.spec.ts Co-Authored-By: claude-flow <ruv@ruv.net>
174 lines
5.7 KiB
TypeScript
174 lines
5.7 KiB
TypeScript
type RuntimeAwareSystemSettings = {
|
|
aiProvider?: string | null;
|
|
azureOpenAiApiKey?: string | null;
|
|
azureDalleApiKey?: string | null;
|
|
geminiApiKey?: string | null;
|
|
smtpPassword?: string | null;
|
|
smtpHost?: string | null;
|
|
smtpPort?: number | null;
|
|
smtpUser?: string | null;
|
|
smtpFrom?: string | null;
|
|
smtpTls?: boolean | null;
|
|
anonymizationSeed?: string | null;
|
|
};
|
|
|
|
export const RUNTIME_SECRET_FIELDS = [
|
|
"azureOpenAiApiKey",
|
|
"azureDalleApiKey",
|
|
"geminiApiKey",
|
|
"smtpPassword",
|
|
"anonymizationSeed",
|
|
] as const;
|
|
|
|
export type RuntimeSecretField = (typeof RUNTIME_SECRET_FIELDS)[number];
|
|
|
|
export type RuntimeSecretStatus = {
|
|
configured: boolean;
|
|
activeSource: "environment" | "database" | "none";
|
|
hasStoredValue: boolean;
|
|
envVarNames: string[];
|
|
};
|
|
|
|
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");
|
|
}
|
|
|
|
function getPrimaryAiEnvVarNames(provider: string | null | undefined): string[] {
|
|
if (provider === "azure") {
|
|
return ["AZURE_OPENAI_API_KEY", "OPENAI_API_KEY"];
|
|
}
|
|
|
|
return ["OPENAI_API_KEY", "AZURE_OPENAI_API_KEY"];
|
|
}
|
|
|
|
function resolveSecretEnvOverride(
|
|
field: RuntimeSecretField,
|
|
provider: string | null | undefined,
|
|
): string | null {
|
|
if (field === "azureOpenAiApiKey") {
|
|
return resolvePrimaryAiApiKey(provider);
|
|
}
|
|
if (field === "azureDalleApiKey") {
|
|
return readEnvOverride("AZURE_DALLE_API_KEY");
|
|
}
|
|
if (field === "geminiApiKey") {
|
|
return readEnvOverride("GEMINI_API_KEY");
|
|
}
|
|
if (field === "smtpPassword") {
|
|
return readEnvOverride("SMTP_PASSWORD");
|
|
}
|
|
|
|
return readEnvOverride("ANONYMIZATION_SEED");
|
|
}
|
|
|
|
function getSecretEnvVarNames(
|
|
field: RuntimeSecretField,
|
|
provider: string | null | undefined,
|
|
): string[] {
|
|
if (field === "azureOpenAiApiKey") {
|
|
return getPrimaryAiEnvVarNames(provider);
|
|
}
|
|
if (field === "azureDalleApiKey") {
|
|
return ["AZURE_DALLE_API_KEY"];
|
|
}
|
|
if (field === "geminiApiKey") {
|
|
return ["GEMINI_API_KEY"];
|
|
}
|
|
if (field === "smtpPassword") {
|
|
return ["SMTP_PASSWORD"];
|
|
}
|
|
|
|
return ["ANONYMIZATION_SEED"];
|
|
}
|
|
|
|
export function getRuntimeSecretStatuses(
|
|
settings: RuntimeAwareSystemSettings | null | undefined,
|
|
): Record<RuntimeSecretField, RuntimeSecretStatus> {
|
|
const provider = settings?.aiProvider;
|
|
|
|
return Object.fromEntries(
|
|
RUNTIME_SECRET_FIELDS.map((field) => {
|
|
const envValue = resolveSecretEnvOverride(field, provider);
|
|
const storedValue = settings?.[field]?.trim() || null;
|
|
const activeSource = envValue
|
|
? "environment"
|
|
: storedValue
|
|
? "database"
|
|
: "none";
|
|
|
|
return [
|
|
field,
|
|
{
|
|
configured: !!(envValue || storedValue),
|
|
activeSource,
|
|
hasStoredValue: !!storedValue,
|
|
envVarNames: getSecretEnvVarNames(field, provider),
|
|
} satisfies RuntimeSecretStatus,
|
|
];
|
|
}),
|
|
) as Record<RuntimeSecretField, RuntimeSecretStatus>;
|
|
}
|
|
|
|
/** Resolve non-secret SMTP fields from ENV (take precedence over DB). */
|
|
function resolveSmtpNonSecretOverrides(settings: RuntimeAwareSystemSettings | null | undefined): {
|
|
smtpHost: string | null;
|
|
smtpPort: number | null;
|
|
smtpUser: string | null;
|
|
smtpFrom: string | null;
|
|
smtpTls: boolean | null;
|
|
} {
|
|
const envHost = readEnvOverride("SMTP_HOST");
|
|
const envPort = readEnvOverride("SMTP_PORT");
|
|
const envUser = readEnvOverride("SMTP_USER");
|
|
const envFrom = readEnvOverride("SMTP_FROM");
|
|
const envTlsRaw = process.env["SMTP_TLS"]?.trim();
|
|
|
|
return {
|
|
smtpHost: envHost ?? settings?.smtpHost ?? null,
|
|
smtpPort: envPort !== null
|
|
? parseInt(envPort, 10)
|
|
: settings?.smtpPort ?? null,
|
|
smtpUser: envUser ?? settings?.smtpUser ?? null,
|
|
smtpFrom: envFrom ?? settings?.smtpFrom ?? null,
|
|
smtpTls: envTlsRaw !== undefined
|
|
? envTlsRaw !== "false"
|
|
: settings?.smtpTls ?? null,
|
|
};
|
|
}
|
|
|
|
export function resolveSystemSettingsRuntime<T extends RuntimeAwareSystemSettings>(
|
|
settings: T | null | undefined,
|
|
): T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "smtpHost" | "smtpPort" | "smtpUser" | "smtpFrom" | "smtpTls" | "anonymizationSeed">> {
|
|
const resolved = { ...(settings ?? {}) } as T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "smtpHost" | "smtpPort" | "smtpUser" | "smtpFrom" | "smtpTls" | "anonymizationSeed">>;
|
|
|
|
resolved.azureOpenAiApiKey = resolveSecretEnvOverride("azureOpenAiApiKey", resolved.aiProvider) ?? settings?.azureOpenAiApiKey ?? null;
|
|
resolved.azureDalleApiKey = resolveSecretEnvOverride("azureDalleApiKey", resolved.aiProvider) ?? settings?.azureDalleApiKey ?? null;
|
|
resolved.geminiApiKey = resolveSecretEnvOverride("geminiApiKey", resolved.aiProvider) ?? settings?.geminiApiKey ?? null;
|
|
resolved.smtpPassword = resolveSecretEnvOverride("smtpPassword", resolved.aiProvider) ?? settings?.smtpPassword ?? null;
|
|
resolved.anonymizationSeed = resolveSecretEnvOverride("anonymizationSeed", resolved.aiProvider) ?? settings?.anonymizationSeed ?? null;
|
|
|
|
const smtpOverrides = resolveSmtpNonSecretOverrides(settings);
|
|
resolved.smtpHost = smtpOverrides.smtpHost;
|
|
resolved.smtpPort = smtpOverrides.smtpPort;
|
|
resolved.smtpUser = smtpOverrides.smtpUser;
|
|
resolved.smtpFrom = smtpOverrides.smtpFrom;
|
|
resolved.smtpTls = smtpOverrides.smtpTls;
|
|
|
|
return resolved;
|
|
}
|