mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-22 22:02:00 +00:00
feat: enhance SMTP configuration options in settings
- Added new fields for smtpSecure and smtpNoAuth in the email settings, allowing users to specify the connection security method and disable authentication for internal servers. - Updated the EmailService to handle the new configuration options, improving flexibility in SMTP setup. - Enhanced the UI components to include the new fields, ensuring proper user interaction and validation. - Updated translation files to include descriptions and titles for the new SMTP settings, improving localization support.
This commit is contained in:
@@ -117,6 +117,18 @@ const defaultConfigs = [
|
||||
type: "string",
|
||||
group: "email",
|
||||
},
|
||||
{
|
||||
key: "smtpSecure",
|
||||
value: "auto",
|
||||
type: "string",
|
||||
group: "email",
|
||||
},
|
||||
{
|
||||
key: "smtpNoAuth",
|
||||
value: "false",
|
||||
type: "boolean",
|
||||
group: "email",
|
||||
},
|
||||
{
|
||||
key: "passwordResetTokenExpiration",
|
||||
value: "3600",
|
||||
|
@@ -148,6 +148,11 @@ export async function appRoutes(app: FastifyInstance) {
|
||||
.describe("SMTP server port (typically 587 for TLS, 25 for non-secure)"),
|
||||
smtpUser: z.string().describe("Username for SMTP authentication (e.g., email address)"),
|
||||
smtpPass: z.string().describe("Password for SMTP authentication (for Gmail, use App Password)"),
|
||||
smtpSecure: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Connection security method ('auto', 'ssl', 'tls', or 'none')"),
|
||||
smtpNoAuth: z.string().optional().describe("Disable SMTP authentication ('true' or 'false')"),
|
||||
})
|
||||
.optional()
|
||||
.describe("SMTP configuration to test. If not provided, uses currently saved configuration"),
|
||||
|
@@ -7,6 +7,8 @@ interface SmtpConfig {
|
||||
smtpPort: string;
|
||||
smtpUser: string;
|
||||
smtpPass: string;
|
||||
smtpSecure?: string;
|
||||
smtpNoAuth?: string;
|
||||
}
|
||||
|
||||
export class EmailService {
|
||||
@@ -18,15 +20,46 @@ export class EmailService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodemailer.createTransport({
|
||||
const port = Number(await this.configService.getValue("smtpPort"));
|
||||
const smtpSecure = (await this.configService.getValue("smtpSecure")) || "auto";
|
||||
const smtpNoAuth = await this.configService.getValue("smtpNoAuth");
|
||||
|
||||
let secure = false;
|
||||
let requireTLS = false;
|
||||
|
||||
if (smtpSecure === "ssl") {
|
||||
secure = true;
|
||||
} else if (smtpSecure === "tls") {
|
||||
requireTLS = true;
|
||||
} else if (smtpSecure === "none") {
|
||||
secure = false;
|
||||
requireTLS = false;
|
||||
} else if (smtpSecure === "auto") {
|
||||
if (port === 465) {
|
||||
secure = true;
|
||||
} else if (port === 587 || port === 25) {
|
||||
requireTLS = true;
|
||||
}
|
||||
}
|
||||
|
||||
const transportConfig: any = {
|
||||
host: await this.configService.getValue("smtpHost"),
|
||||
port: Number(await this.configService.getValue("smtpPort")),
|
||||
secure: false,
|
||||
auth: {
|
||||
port: port,
|
||||
secure: secure,
|
||||
requireTLS: requireTLS,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (smtpNoAuth !== "true") {
|
||||
transportConfig.auth = {
|
||||
user: await this.configService.getValue("smtpUser"),
|
||||
pass: await this.configService.getValue("smtpPass"),
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return nodemailer.createTransport(transportConfig);
|
||||
}
|
||||
|
||||
async testConnection(config?: SmtpConfig) {
|
||||
@@ -43,6 +76,8 @@ export class EmailService {
|
||||
smtpPort: await this.configService.getValue("smtpPort"),
|
||||
smtpUser: await this.configService.getValue("smtpUser"),
|
||||
smtpPass: await this.configService.getValue("smtpPass"),
|
||||
smtpSecure: (await this.configService.getValue("smtpSecure")) || "auto",
|
||||
smtpNoAuth: await this.configService.getValue("smtpNoAuth"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,15 +85,46 @@ export class EmailService {
|
||||
throw new Error("SMTP is not enabled");
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
const port = Number(smtpConfig.smtpPort);
|
||||
const smtpSecure = smtpConfig.smtpSecure || "auto";
|
||||
const smtpNoAuth = smtpConfig.smtpNoAuth;
|
||||
|
||||
let secure = false;
|
||||
let requireTLS = false;
|
||||
|
||||
if (smtpSecure === "ssl") {
|
||||
secure = true;
|
||||
} else if (smtpSecure === "tls") {
|
||||
requireTLS = true;
|
||||
} else if (smtpSecure === "none") {
|
||||
secure = false;
|
||||
requireTLS = false;
|
||||
} else if (smtpSecure === "auto") {
|
||||
if (port === 465) {
|
||||
secure = true;
|
||||
} else if (port === 587 || port === 25) {
|
||||
requireTLS = true;
|
||||
}
|
||||
}
|
||||
|
||||
const transportConfig: any = {
|
||||
host: smtpConfig.smtpHost,
|
||||
port: Number(smtpConfig.smtpPort),
|
||||
secure: false,
|
||||
auth: {
|
||||
port: port,
|
||||
secure: secure,
|
||||
requireTLS: requireTLS,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
};
|
||||
|
||||
if (smtpNoAuth !== "true") {
|
||||
transportConfig.auth = {
|
||||
user: smtpConfig.smtpUser,
|
||||
pass: smtpConfig.smtpPass,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(transportConfig);
|
||||
|
||||
try {
|
||||
await transporter.verify();
|
||||
|
@@ -465,6 +465,20 @@
|
||||
"title": "Sender Email",
|
||||
"description": "Sender email address"
|
||||
},
|
||||
"smtpSecure": {
|
||||
"title": "Connection Security",
|
||||
"description": "SMTP connection security method - Auto (recommended), SSL, STARTTLS, or None (insecure)",
|
||||
"options": {
|
||||
"auto": "Auto (Recommended)",
|
||||
"ssl": "SSL (Port 465)",
|
||||
"tls": "STARTTLS (Port 587)",
|
||||
"none": "None (Insecure)"
|
||||
}
|
||||
},
|
||||
"smtpNoAuth": {
|
||||
"title": "No Authentication",
|
||||
"description": "Enable this for internal servers that don't require username/password (hides auth fields)"
|
||||
},
|
||||
"testSmtp": {
|
||||
"title": "Test SMTP Connection",
|
||||
"description": "Test if the SMTP configuration is valid"
|
||||
@@ -548,7 +562,10 @@
|
||||
"updateSuccess": "{group} settings updated successfully",
|
||||
"smtpTestSuccess": "SMTP connection successful! Your email configuration is working correctly.",
|
||||
"smtpTestFailed": "SMTP connection failed: {error}",
|
||||
"smtpTestGenericError": "Failed to test SMTP connection. Please check your settings and try again."
|
||||
"smtpTestGenericError": "Failed to test SMTP connection. Please check your settings and try again.",
|
||||
"smtpNotEnabled": "SMTP is not enabled. Please enable SMTP first.",
|
||||
"smtpMissingHostPort": "Please fill in SMTP Host and Port before testing.",
|
||||
"smtpMissingAuth": "Please fill in SMTP Username and Password, or enable 'No Authentication' option."
|
||||
},
|
||||
"title": "Settings",
|
||||
"breadcrumb": "Settings",
|
||||
@@ -556,7 +573,8 @@
|
||||
"tooltips": {
|
||||
"oidcScope": "Enter a scope and press Enter to add",
|
||||
"oidcAdminEmailDomains": "Enter a domain and press Enter to add",
|
||||
"testSmtp": "Tests the SMTP connection with the values currently entered in the form. To make changes permanent, remember to save your settings after testing."
|
||||
"testSmtp": "Tests the SMTP connection with the values currently entered in the form. To make changes permanent, remember to save your settings after testing.",
|
||||
"defaultPlaceholder": "Enter and press Enter"
|
||||
},
|
||||
"redirectUri": {
|
||||
"placeholder": "https://mysite.com",
|
||||
|
@@ -51,42 +51,71 @@ export function SettingsGroup({ group, configs, form, isCollapsed, onToggleColla
|
||||
<div className="flex flex-col gap-4">
|
||||
{configs
|
||||
.filter((config) => !isFieldHidden(config.key))
|
||||
.map((config) => (
|
||||
<div key={config.key} className="space-y-2 mb-3">
|
||||
<SettingsInput
|
||||
config={config}
|
||||
error={form.formState.errors.configs?.[config.key]}
|
||||
register={form.register}
|
||||
setValue={form.setValue}
|
||||
smtpEnabled={form.watch("configs.smtpEnabled")}
|
||||
oidcEnabled={form.watch("configs.oidcEnabled")}
|
||||
watch={form.watch}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground ml-1">
|
||||
{t(`settings.fields.${config.key}.description`, {
|
||||
defaultValue:
|
||||
FIELD_DESCRIPTIONS[config.key as keyof typeof FIELD_DESCRIPTIONS] ||
|
||||
config.description ||
|
||||
t("settings.fields.noDescription"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
.map((config) => {
|
||||
const smtpEnabled = form.watch("configs.smtpEnabled");
|
||||
const smtpNoAuth = form.watch("configs.smtpNoAuth");
|
||||
const isSmtpAuthField = config.key === "smtpUser" || config.key === "smtpPass";
|
||||
|
||||
const smtpFields = [
|
||||
"smtpHost",
|
||||
"smtpPort",
|
||||
"smtpUser",
|
||||
"smtpPass",
|
||||
"smtpSecure",
|
||||
"smtpNoAuth",
|
||||
"smtpFromName",
|
||||
"smtpFromEmail",
|
||||
];
|
||||
|
||||
if (smtpEnabled !== "true" && smtpFields.includes(config.key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isSmtpAuthField && smtpNoAuth === "true") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={config.key} className="space-y-2 mb-3">
|
||||
<SettingsInput
|
||||
config={config}
|
||||
error={form.formState.errors.configs?.[config.key]}
|
||||
register={form.register}
|
||||
setValue={form.setValue}
|
||||
smtpEnabled={form.watch("configs.smtpEnabled")}
|
||||
oidcEnabled={form.watch("configs.oidcEnabled")}
|
||||
watch={form.watch}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground ml-1">
|
||||
{t(`settings.fields.${config.key}.description`, {
|
||||
defaultValue:
|
||||
FIELD_DESCRIPTIONS[config.key as keyof typeof FIELD_DESCRIPTIONS] ||
|
||||
config.description ||
|
||||
t("settings.fields.noDescription"),
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-4">
|
||||
{isEmailGroup && (
|
||||
<SmtpTestButton
|
||||
smtpEnabled={form.watch("configs.smtpEnabled") || "false"}
|
||||
getFormValues={() => ({
|
||||
smtpEnabled: form.getValues("configs.smtpEnabled") || "false",
|
||||
smtpHost: form.getValues("configs.smtpHost") || "",
|
||||
smtpPort: form.getValues("configs.smtpPort") || "",
|
||||
smtpUser: form.getValues("configs.smtpUser") || "",
|
||||
smtpPass: form.getValues("configs.smtpPass") || "",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
<div className={`flex ${isEmailGroup ? "ml-auto" : ""}`}>
|
||||
<div className="flex">
|
||||
{isEmailGroup && form.watch("configs.smtpEnabled") === "true" && (
|
||||
<SmtpTestButton
|
||||
smtpEnabled={form.watch("configs.smtpEnabled") || "false"}
|
||||
getFormValues={() => ({
|
||||
smtpEnabled: form.getValues("configs.smtpEnabled") || "false",
|
||||
smtpHost: form.getValues("configs.smtpHost") || "",
|
||||
smtpPort: form.getValues("configs.smtpPort") || "",
|
||||
smtpUser: form.getValues("configs.smtpUser") || "",
|
||||
smtpPass: form.getValues("configs.smtpPass") || "",
|
||||
smtpSecure: form.getValues("configs.smtpSecure") || "auto",
|
||||
smtpNoAuth: form.getValues("configs.smtpNoAuth") || "false",
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={form.formState.isSubmitting}
|
||||
|
@@ -2,8 +2,10 @@ import { IconInfoCircle } from "@tabler/icons-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { UseFormRegister, UseFormSetValue, UseFormWatch } from "react-hook-form";
|
||||
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { TagsInput } from "@/components/ui/tags-input";
|
||||
import { createFieldTitles } from "../constants";
|
||||
import { Config } from "../types";
|
||||
@@ -129,7 +131,7 @@ export function SettingsInput({
|
||||
? "openid profile email"
|
||||
: config.key === "oidcAdminEmailDomains"
|
||||
? "admin.com company.org"
|
||||
: "Digite e pressione Enter"
|
||||
: t("settings.tooltips.defaultPlaceholder", { defaultValue: "Enter and press Enter" })
|
||||
}
|
||||
/>
|
||||
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
|
||||
@@ -137,6 +139,73 @@ export function SettingsInput({
|
||||
);
|
||||
}
|
||||
|
||||
if (config.key === "smtpEnabled") {
|
||||
const currentValue = watch(`configs.${config.key}`) === "true";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Switch
|
||||
id={`configs.${config.key}`}
|
||||
checked={currentValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue(`configs.${config.key}`, checked ? "true" : "false", { shouldDirty: true });
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<label htmlFor={`configs.${config.key}`} className="text-sm font-semibold cursor-pointer">
|
||||
{friendlyLabel}
|
||||
</label>
|
||||
</div>
|
||||
{config.description && <p className="text-xs text-muted-foreground ml-11">{config.description}</p>}
|
||||
{error && <p className="text-danger text-xs mt-1 ml-11">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.key === "smtpSecure") {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-semibold">{friendlyLabel}</label>
|
||||
<select
|
||||
{...register(`configs.${config.key}`)}
|
||||
className="w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<option value="auto">{t("settings.fields.smtpSecure.options.auto")}</option>
|
||||
<option value="ssl">{t("settings.fields.smtpSecure.options.ssl")}</option>
|
||||
<option value="tls">{t("settings.fields.smtpSecure.options.tls")}</option>
|
||||
<option value="none">{t("settings.fields.smtpSecure.options.none")}</option>
|
||||
</select>
|
||||
{error && <p className="text-danger text-xs mt-1">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.key === "smtpNoAuth") {
|
||||
const currentValue = watch(`configs.${config.key}`) === "true";
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Checkbox
|
||||
id={`configs.${config.key}`}
|
||||
checked={currentValue}
|
||||
onCheckedChange={(checked) => {
|
||||
setValue(`configs.${config.key}`, checked ? "true" : "false", { shouldDirty: true });
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<label htmlFor={`configs.${config.key}`} className="text-sm font-semibold cursor-pointer">
|
||||
{friendlyLabel}
|
||||
</label>
|
||||
</div>
|
||||
{config.description && <p className="text-xs text-muted-foreground ml-7">{config.description}</p>}
|
||||
{error && <p className="text-danger text-xs mt-1 ml-7">{error.message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (config.type) {
|
||||
case "boolean":
|
||||
return (
|
||||
|
@@ -16,6 +16,8 @@ interface SmtpTestButtonProps {
|
||||
smtpPort: string;
|
||||
smtpUser: string;
|
||||
smtpPass: string;
|
||||
smtpSecure: string;
|
||||
smtpNoAuth: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,13 +29,17 @@ export function SmtpTestButton({ smtpEnabled, getFormValues }: SmtpTestButtonPro
|
||||
const formValues = getFormValues();
|
||||
|
||||
if (formValues.smtpEnabled !== "true") {
|
||||
toast.error("SMTP is not enabled. Please enable SMTP first.");
|
||||
toast.error(t("settings.messages.smtpNotEnabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if required fields are filled
|
||||
if (!formValues.smtpHost || !formValues.smtpPort || !formValues.smtpUser || !formValues.smtpPass) {
|
||||
toast.error("Please fill in all SMTP configuration fields before testing.");
|
||||
if (!formValues.smtpHost || !formValues.smtpPort) {
|
||||
toast.error(t("settings.messages.smtpMissingHostPort"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (formValues.smtpNoAuth !== "true" && (!formValues.smtpUser || !formValues.smtpPass)) {
|
||||
toast.error(t("settings.messages.smtpMissingAuth"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -46,6 +52,8 @@ export function SmtpTestButton({ smtpEnabled, getFormValues }: SmtpTestButtonPro
|
||||
smtpPort: formValues.smtpPort,
|
||||
smtpUser: formValues.smtpUser,
|
||||
smtpPass: formValues.smtpPass,
|
||||
smtpSecure: formValues.smtpSecure,
|
||||
smtpNoAuth: formValues.smtpNoAuth,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -55,7 +63,7 @@ export function SmtpTestButton({ smtpEnabled, getFormValues }: SmtpTestButtonPro
|
||||
toast.error(t("settings.messages.smtpTestGenericError"));
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.response?.data?.error || error?.message || "Unknown error";
|
||||
const errorMessage = error?.response?.data?.error || error?.message || t("common.unexpectedError");
|
||||
toast.error(t("settings.messages.smtpTestFailed", { error: errorMessage }));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
|
@@ -46,6 +46,8 @@ export const createFieldDescriptions = (t: ReturnType<typeof createTranslator>)
|
||||
smtpPass: t("settings.fields.smtpPass.description"),
|
||||
smtpFromName: t("settings.fields.smtpFromName.description"),
|
||||
smtpFromEmail: t("settings.fields.smtpFromEmail.description"),
|
||||
smtpSecure: t("settings.fields.smtpSecure.description"),
|
||||
smtpNoAuth: t("settings.fields.smtpNoAuth.description"),
|
||||
|
||||
// OIDC settings (nomes corretos do seed)
|
||||
oidcEnabled: t("settings.fields.oidcEnabled.description"),
|
||||
@@ -85,6 +87,8 @@ export const createFieldTitles = (t: ReturnType<typeof createTranslator>) => ({
|
||||
smtpPass: t("settings.fields.smtpPass.title"),
|
||||
smtpFromName: t("settings.fields.smtpFromName.title"),
|
||||
smtpFromEmail: t("settings.fields.smtpFromEmail.title"),
|
||||
smtpSecure: t("settings.fields.smtpSecure.title"),
|
||||
smtpNoAuth: t("settings.fields.smtpNoAuth.title"),
|
||||
|
||||
// OIDC settings
|
||||
oidcEnabled: t("settings.fields.oidcEnabled.title"),
|
||||
|
@@ -77,8 +77,26 @@ export function useSettings() {
|
||||
}
|
||||
|
||||
if (group === "email") {
|
||||
if (a.key === "smtpEnabled") return -1;
|
||||
if (b.key === "smtpEnabled") return 1;
|
||||
const smtpOrder = [
|
||||
"smtpEnabled",
|
||||
"smtpHost",
|
||||
"smtpPort",
|
||||
"smtpSecure",
|
||||
"smtpNoAuth",
|
||||
"smtpUser",
|
||||
"smtpPass",
|
||||
"smtpFromName",
|
||||
"smtpFromEmail",
|
||||
];
|
||||
|
||||
const aIndex = smtpOrder.indexOf(a.key);
|
||||
const bIndex = smtpOrder.indexOf(b.key);
|
||||
|
||||
if (aIndex !== -1 && bIndex !== -1) {
|
||||
return aIndex - bIndex;
|
||||
}
|
||||
if (aIndex !== -1) return -1;
|
||||
if (bIndex !== -1) return 1;
|
||||
}
|
||||
|
||||
if (group === "oidc") {
|
||||
|
@@ -90,6 +90,8 @@ export interface TestSmtpConnectionBody {
|
||||
smtpPort: string;
|
||||
smtpUser: string;
|
||||
smtpPass: string;
|
||||
smtpSecure: string;
|
||||
smtpNoAuth: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ docker buildx build \
|
||||
--no-cache \
|
||||
-t kyantech/palmr:latest \
|
||||
-t kyantech/palmr:$TAG \
|
||||
--load \
|
||||
--push \
|
||||
.
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
|
Reference in New Issue
Block a user