email config
This commit is contained in:
1 parent
60153a88e2
commit
af6fede1b3
7 files changed
+179
-61
No files matched your search
@@ -26,8 +26,8 @@ export const config: IEnvConfig = {
|
||||
JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret",
|
||||
PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
|
||||
PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "",
|
||||
SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com",
|
||||
SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587,
|
||||
SMTP_USER: process.env.SMTP_USER || "user@example.com",
|
||||
SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password",
|
||||
SMTP_HOST: process.env.SMTP_HOST || "not_set",
|
||||
SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : -1,
|
||||
SMTP_USER: process.env.SMTP_USER || "not_set",
|
||||
SMTP_PASS: process.env.SMTP_PASS || "not_set",
|
||||
};
|
||||
@@ -4,7 +4,11 @@ import { NotificationService, SettingsService } from "@/services/index.js";
|
||||
|
||||
export interface ISettingsController {
|
||||
get: (req: Request, res: Response, next: NextFunction) => void;
|
||||
update: (req: Request, res: Response, next: NextFunction) => void;
|
||||
updateEmailSettings: (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => void;
|
||||
testTransport: (req: Request, res: Response, next: NextFunction) => void;
|
||||
}
|
||||
|
||||
@@ -31,11 +35,17 @@ class SettingsController implements ISettingsController {
|
||||
}
|
||||
};
|
||||
|
||||
update = async (req: Request, res: Response, next: NextFunction) => {
|
||||
updateEmailSettings = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) => {
|
||||
try {
|
||||
const updatedSettings = await this.settingsService.update(req.body);
|
||||
const updatedSettings = await this.settingsService.updateEmailSettings(
|
||||
req.body
|
||||
);
|
||||
res.status(200).json({
|
||||
message: "Settings updated successfully",
|
||||
message: "Email settings updated successfully",
|
||||
data: updatedSettings,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -38,12 +38,17 @@ export const initServices = async () => {
|
||||
const notificationChannelService = new NotificationChannelService();
|
||||
const userService = new UserService();
|
||||
const discordService = new DiscordService();
|
||||
const emailService = new EmailService(userService);
|
||||
const slackService = new SlackService();
|
||||
const webhookService = new WebhookService();
|
||||
const networkService = new NetworkService(got);
|
||||
const statusService = new StatusService();
|
||||
const notificationService = new NotificationService(userService);
|
||||
const settingsService = new SettingsService();
|
||||
const emailService = new EmailService(userService, settingsService);
|
||||
settingsService.setEmailService(emailService);
|
||||
const notificationService = new NotificationService(
|
||||
userService,
|
||||
settingsService
|
||||
);
|
||||
const incidentService = new IncidentService();
|
||||
const jobGenerator = new JobGenerator(
|
||||
networkService,
|
||||
@@ -64,8 +69,6 @@ export const initServices = async () => {
|
||||
const statusPageService = new StatusPageService();
|
||||
const diagnosticService = new DiagnosticService(jobQueue);
|
||||
const recoveryService = new RecoveryService();
|
||||
const settingsService = new SettingsService();
|
||||
|
||||
const services = {
|
||||
checkService,
|
||||
inviteService,
|
||||
|
||||
@@ -27,12 +27,12 @@ class SettingsRoutes {
|
||||
);
|
||||
|
||||
this.router.patch(
|
||||
"/",
|
||||
"/email",
|
||||
verifyToken,
|
||||
addUserContext,
|
||||
verifyTeamPermission([PERMISSIONS.master]),
|
||||
validateBody(systemSettingsSchema),
|
||||
this.controller.update
|
||||
this.controller.updateEmailSettings
|
||||
);
|
||||
this.router.post(
|
||||
"/test-transport",
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
DiscordService,
|
||||
WebhookService,
|
||||
} from "./NotificationServices/index.js";
|
||||
import SettingsService from "@/services/system/SettingsService.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import { getChildLogger } from "@/logger/Logger.js";
|
||||
import type { IEmailTransport } from "./NotificationServices/Email.js";
|
||||
@@ -37,7 +38,7 @@ export interface INotificationService {
|
||||
testNotificationChannel: (
|
||||
notificationChannel: INotificationChannel
|
||||
) => Promise<Boolean>;
|
||||
testTransport: (transport: IEmailTransport) => Promise<boolean>;
|
||||
testEmailTransport: (transport: IEmailTransport) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class NotificationService implements INotificationService {
|
||||
@@ -48,10 +49,10 @@ class NotificationService implements INotificationService {
|
||||
private webhookService: WebhookService;
|
||||
private userService: UserService;
|
||||
|
||||
constructor(userService: UserService) {
|
||||
constructor(userService: UserService, settingsService: SettingsService) {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.userService = userService;
|
||||
this.emailService = new EmailService(userService);
|
||||
this.emailService = new EmailService(userService, settingsService);
|
||||
this.slackService = new SlackService();
|
||||
this.discordService = new DiscordService();
|
||||
this.webhookService = new WebhookService();
|
||||
@@ -111,6 +112,7 @@ class NotificationService implements INotificationService {
|
||||
case "email":
|
||||
const sentEmail = await this.emailService.testMessage(channel);
|
||||
results.push({
|
||||
channelId: channel._id,
|
||||
channelName: channel.name,
|
||||
channelType: channel.type,
|
||||
channelUrl: channel.config?.emailAddress || "N/A",
|
||||
@@ -120,6 +122,7 @@ class NotificationService implements INotificationService {
|
||||
case "slack":
|
||||
const sentSlack = await this.slackService.testMessage(channel);
|
||||
results.push({
|
||||
channelId: channel._id,
|
||||
channelName: channel.name,
|
||||
channelType: channel.type,
|
||||
channelUrl: channel.config?.url || "N/A",
|
||||
@@ -129,6 +132,7 @@ class NotificationService implements INotificationService {
|
||||
case "discord":
|
||||
const sentDiscord = await this.discordService.testMessage(channel);
|
||||
results.push({
|
||||
channelId: channel._id,
|
||||
channelName: channel.name,
|
||||
channelType: channel.type,
|
||||
channelUrl: channel.config?.url || "N/A",
|
||||
@@ -138,6 +142,7 @@ class NotificationService implements INotificationService {
|
||||
case "webhook":
|
||||
const sentWebhook = await this.webhookService.testMessage(channel);
|
||||
results.push({
|
||||
channelId: channel._id,
|
||||
channelName: channel.name,
|
||||
channelType: channel.type,
|
||||
channelUrl: channel.config?.url || "N/A",
|
||||
@@ -179,7 +184,7 @@ class NotificationService implements INotificationService {
|
||||
const result: any[] = [];
|
||||
await this.testNotification(notificationChannel, result);
|
||||
if (result.length > 0) {
|
||||
return true;
|
||||
return result.find((r) => r._id === notificationChannel._id)?.sent;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ import nodemailer, { Transporter } from "nodemailer";
|
||||
import { config } from "@/config/index.js";
|
||||
import UserService from "../../business/UserService.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import { SettingsService } from "@/services/index.js";
|
||||
import { getChildLogger } from "@/logger/Logger.js";
|
||||
import { transport } from "winston";
|
||||
const SERVICE_NAME = "EmailService";
|
||||
const logger = getChildLogger(SERVICE_NAME);
|
||||
|
||||
@@ -34,17 +36,74 @@ export interface IEmailService extends IMessageService {
|
||||
content: string
|
||||
) => Promise<boolean>;
|
||||
testTransport: (transport: IEmailTransport) => Promise<boolean>;
|
||||
rebuildTransport: (transport: IEmailTransport) => Promise<boolean>;
|
||||
}
|
||||
|
||||
class EmailService implements IEmailService {
|
||||
public SERVICE_NAME = SERVICE_NAME;
|
||||
private transporter: Transporter;
|
||||
private transporter: Transporter | null;
|
||||
private userService: UserService;
|
||||
private settingsService: SettingsService;
|
||||
private from: string | null;
|
||||
|
||||
constructor(userService: UserService) {
|
||||
constructor(userService: UserService, settingsService: SettingsService) {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
this.userService = userService;
|
||||
this.transporter = nodemailer.createTransport({
|
||||
this.settingsService = settingsService;
|
||||
this.transporter = null;
|
||||
this.from = null;
|
||||
}
|
||||
|
||||
private hasEnvConfig = () => {
|
||||
return (
|
||||
config.SMTP_HOST !== "not_set" ||
|
||||
config.SMTP_PORT !== -1 ||
|
||||
config.SMTP_USER !== "not_set" ||
|
||||
config.SMTP_PASS !== "not_set"
|
||||
);
|
||||
};
|
||||
|
||||
private buildSystemSettingsTransport = async (transport: IEmailTransport) => {
|
||||
const host = (transport.systemEmailHost ?? "").trim();
|
||||
const port =
|
||||
transport.systemEmailPort !== undefined &&
|
||||
transport.systemEmailPort !== null
|
||||
? Number(transport.systemEmailPort)
|
||||
: undefined;
|
||||
const baseOptions = {
|
||||
port,
|
||||
secure: transport.systemEmailSecure,
|
||||
auth: {
|
||||
user: transport.systemEmailUser || transport.systemEmailAddress,
|
||||
pass: transport.systemEmailPassword,
|
||||
},
|
||||
name: transport.systemEmailConnectionHost || undefined,
|
||||
connectionTimeout: 5000,
|
||||
tls: {
|
||||
rejectUnauthorized: transport.systemEmailRejectUnauthorized,
|
||||
ignoreTLS: transport.systemEmailIgnoreTLS,
|
||||
requireTLS: transport.systemEmailRequireTLS,
|
||||
servername: transport.systemEmailTLSServername,
|
||||
},
|
||||
};
|
||||
|
||||
const transportOptions = transport.systemEmailPool
|
||||
? {
|
||||
...baseOptions,
|
||||
pool: true,
|
||||
host,
|
||||
}
|
||||
: {
|
||||
...baseOptions,
|
||||
host: host || undefined,
|
||||
};
|
||||
this.from = transport.systemEmailAddress || null;
|
||||
return nodemailer.createTransport(transportOptions);
|
||||
};
|
||||
|
||||
private buildEnvTransport = () => {
|
||||
this.from = config.SMTP_USER || null;
|
||||
return nodemailer.createTransport({
|
||||
host: config.SMTP_HOST,
|
||||
port: config.SMTP_PORT,
|
||||
secure: config.SMTP_PORT === 465,
|
||||
@@ -53,7 +112,16 @@ class EmailService implements IEmailService {
|
||||
pass: config.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private buildTransport = async () => {
|
||||
if (this.hasEnvConfig()) {
|
||||
return this.buildEnvTransport();
|
||||
} else {
|
||||
const systemSettings = await this.settingsService.get();
|
||||
return this.buildSystemSettingsTransport(systemSettings);
|
||||
}
|
||||
};
|
||||
|
||||
buildAlert = (monitor: IMonitor, incident: IIncident) => {
|
||||
const name = monitor?.name || "Unnamed monitor";
|
||||
@@ -83,20 +151,26 @@ class EmailService implements IEmailService {
|
||||
throw new ApiError("No user emails found", 500);
|
||||
}
|
||||
|
||||
await this.transporter.sendMail({
|
||||
from: `"Checkmate" <${config.SMTP_USER}>`,
|
||||
if (this.transporter === null) {
|
||||
this.transporter = await this.buildTransport();
|
||||
}
|
||||
|
||||
const res = await this.transporter.sendMail({
|
||||
from: `"Checkmate" <${this.from}>`,
|
||||
to: emails,
|
||||
subject: "Monitor Alert",
|
||||
text: JSON.stringify(alert, null, 2),
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
testMessage = async (channel: INotificationChannel) => {
|
||||
return this.sendMessage(
|
||||
return await this.sendMessage(
|
||||
{
|
||||
name: "This is a test",
|
||||
url: "Test URL",
|
||||
@@ -114,8 +188,12 @@ class EmailService implements IEmailService {
|
||||
|
||||
sendGeneric = async (to: string, subject: string, content: string) => {
|
||||
try {
|
||||
if (this.transporter === null) {
|
||||
this.transporter = await this.buildTransport();
|
||||
}
|
||||
|
||||
await this.transporter.sendMail({
|
||||
from: `"Checkmate" <${config.SMTP_USER}>`,
|
||||
from: `"Checkmate" <${this.from}>`,
|
||||
to: to,
|
||||
subject: subject,
|
||||
text: content,
|
||||
@@ -127,36 +205,13 @@ class EmailService implements IEmailService {
|
||||
};
|
||||
|
||||
testTransport = async (transport: IEmailTransport) => {
|
||||
const host = (transport.systemEmailHost ?? "").trim();
|
||||
const baseOptions = {
|
||||
port: Number(transport.systemEmailPort),
|
||||
secure: transport.systemEmailSecure,
|
||||
auth: {
|
||||
user: transport.systemEmailUser || transport.systemEmailAddress,
|
||||
pass: transport.systemEmailPassword,
|
||||
},
|
||||
name: transport.systemEmailConnectionHost || undefined,
|
||||
connectionTimeout: 5000,
|
||||
tls: {
|
||||
rejectUnauthorized: transport.systemEmailRejectUnauthorized,
|
||||
ignoreTLS: transport.systemEmailIgnoreTLS,
|
||||
requireTLS: transport.systemEmailRequireTLS,
|
||||
servername: transport.systemEmailTLSServername,
|
||||
},
|
||||
};
|
||||
|
||||
const transportOptions = transport.systemEmailPool
|
||||
? {
|
||||
...baseOptions,
|
||||
pool: true,
|
||||
host,
|
||||
}
|
||||
: {
|
||||
...baseOptions,
|
||||
host: host || undefined,
|
||||
};
|
||||
|
||||
const testTransport = nodemailer.createTransport(transportOptions);
|
||||
if (this.hasEnvConfig()) {
|
||||
throw new ApiError(
|
||||
"Cannot test transport when environment SMTP settings are set",
|
||||
400
|
||||
);
|
||||
}
|
||||
const testTransport = await this.buildSystemSettingsTransport(transport);
|
||||
try {
|
||||
await testTransport.sendMail({
|
||||
from: `"Checkmate Test" <${transport.systemEmailAddress}>`,
|
||||
@@ -170,6 +225,12 @@ class EmailService implements IEmailService {
|
||||
throw new ApiError(`Email transport test failed`, 400);
|
||||
}
|
||||
};
|
||||
|
||||
rebuildTransport = async (transport: IEmailTransport) => {
|
||||
this.transporter = await this.buildSystemSettingsTransport(transport);
|
||||
this.from = transport.systemEmailAddress || null;
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export default EmailService;
|
||||
@@ -1,19 +1,26 @@
|
||||
import { ISystemSettings, SystemSettings } from "@/db/models/index.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
|
||||
import { EmailService } from "@/services/index.js";
|
||||
const SERVICE_NAME = "SettingsService";
|
||||
export interface ISettingsService {
|
||||
get: () => Promise<ISystemSettings>;
|
||||
update: (settings: Partial<ISystemSettings>) => Promise<ISystemSettings>;
|
||||
updateEmailSettings: (
|
||||
settings: Partial<ISystemSettings>
|
||||
) => Promise<ISystemSettings>;
|
||||
}
|
||||
|
||||
class SettingsService implements ISettingsService {
|
||||
public SERVICE_NAME: string;
|
||||
private emailService!: EmailService;
|
||||
|
||||
constructor() {
|
||||
this.SERVICE_NAME = SERVICE_NAME;
|
||||
}
|
||||
|
||||
setEmailService(emailService: EmailService) {
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
get = async (): Promise<ISystemSettings> => {
|
||||
let settings = await SystemSettings.findById("global").select(
|
||||
"-_id -__v -createdAt -updatedAt"
|
||||
@@ -34,17 +41,47 @@ class SettingsService implements ISettingsService {
|
||||
return settings;
|
||||
};
|
||||
|
||||
update = async (
|
||||
updateEmailSettings = async (
|
||||
payload: Partial<ISystemSettings>
|
||||
): Promise<ISystemSettings> => {
|
||||
const { _id, createdAt, updatedAt, ...updates } = payload as Record<
|
||||
const allFields = [
|
||||
"systemEmailHost",
|
||||
"systemEmailPort",
|
||||
"systemEmailAddress",
|
||||
"systemEmailPassword",
|
||||
"systemEmailUser",
|
||||
"systemEmailConnectionHost",
|
||||
"systemEmailTLSServername",
|
||||
"systemEmailSecure",
|
||||
"systemEmailPool",
|
||||
"systemEmailIgnoreTLS",
|
||||
"systemEmailRequireTLS",
|
||||
"systemEmailRejectUnauthorized",
|
||||
] as const;
|
||||
|
||||
const { _id, createdAt, updatedAt, ...incoming } = payload as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
|
||||
const updateDoc: {
|
||||
$set?: Record<string, unknown>;
|
||||
$unset?: Record<string, 1>;
|
||||
} = {};
|
||||
|
||||
for (const field of allFields) {
|
||||
if (Object.prototype.hasOwnProperty.call(incoming, field)) {
|
||||
updateDoc.$set ??= {};
|
||||
updateDoc.$set[field] = incoming[field];
|
||||
} else {
|
||||
updateDoc.$unset ??= {};
|
||||
updateDoc.$unset[field] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await SystemSettings.findOneAndUpdate(
|
||||
{ _id: "global" },
|
||||
{ $set: updates },
|
||||
updateDoc,
|
||||
{ new: true, upsert: true, setDefaultsOnInsert: true }
|
||||
);
|
||||
|
||||
@@ -52,6 +89,8 @@ class SettingsService implements ISettingsService {
|
||||
throw new ApiError("Unable to update system settings", 500);
|
||||
}
|
||||
|
||||
this.emailService.rebuildTransport(settings);
|
||||
|
||||
return settings;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user