feat: SMTP full ENV override, password reset flow, and E2E email testing
- 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>
This commit is contained in:
@@ -51,9 +51,14 @@ async function getSmtpConfig() {
|
||||
await db.systemSettings.findUnique({ where: { id: "singleton" } }),
|
||||
);
|
||||
if (!settings.smtpHost) return null;
|
||||
const port = typeof settings.smtpPort === "number"
|
||||
? settings.smtpPort
|
||||
: settings.smtpPort !== null && settings.smtpPort !== undefined
|
||||
? parseInt(String(settings.smtpPort), 10)
|
||||
: 587;
|
||||
return {
|
||||
host: settings.smtpHost,
|
||||
port: settings.smtpPort ?? 587,
|
||||
port,
|
||||
secure: settings.smtpTls === false ? false : true,
|
||||
auth:
|
||||
settings.smtpUser && settings.smtpPassword
|
||||
|
||||
@@ -4,6 +4,11 @@ type RuntimeAwareSystemSettings = {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -119,10 +124,37 @@ export function getRuntimeSecretStatuses(
|
||||
) 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" | "anonymizationSeed">> {
|
||||
const resolved = { ...(settings ?? {}) } as T & Required<Pick<RuntimeAwareSystemSettings, "azureOpenAiApiKey" | "azureDalleApiKey" | "geminiApiKey" | "smtpPassword" | "anonymizationSeed">>;
|
||||
): 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;
|
||||
@@ -130,5 +162,12 @@ export function resolveSystemSettingsRuntime<T extends RuntimeAwareSystemSetting
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user