feat: Add ability to configure and test SMTP via UI
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /bootstrap:
|
||||
* get:
|
||||
* summary: Get application bootstrap
|
||||
* description: Retrieves the application bootstrap.
|
||||
* tags:
|
||||
* - Bootstrap
|
||||
* operationId: getBootstrap
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Bootstrap retrieved successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - oidc
|
||||
* - version
|
||||
* properties:
|
||||
* oidc:
|
||||
* type: object
|
||||
* required:
|
||||
* - authorizationUrl
|
||||
* - endSessionUrl
|
||||
* - isEnforced
|
||||
* nullable: true
|
||||
* description: OpenID Connect configuration (null if not configured)
|
||||
* properties:
|
||||
* authorizationUrl:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: OIDC authorization URL for initiating authentication
|
||||
* example: https://oidc.example.com/auth
|
||||
* endSessionUrl:
|
||||
* type: string
|
||||
* format: uri
|
||||
* nullable: true
|
||||
* description: OIDC end session URL for logout (null if not supported by provider)
|
||||
* example: https://oidc.example.com/logout
|
||||
* isEnforced:
|
||||
* type: boolean
|
||||
* description: Whether OIDC authentication is enforced (users must use OIDC to login)
|
||||
* example: false
|
||||
* activeUsersLimit:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* description: Maximum number of active users allowed (conditionally added for admins if configured)
|
||||
* example: 100
|
||||
* version:
|
||||
* type: string
|
||||
* description: Current version of the PLANKA application
|
||||
* example: 2.0.0
|
||||
* security: []
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
async fn() {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const oidc = await sails.hooks.oidc.getBootstrap();
|
||||
|
||||
return {
|
||||
item: sails.helpers.bootstrap.presentOne(oidc, currentUser),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@
|
||||
* /config:
|
||||
* get:
|
||||
* summary: Get application configuration
|
||||
* description: Retrieves the application configuration.
|
||||
* description: Retrieves the application configuration. Requires admin privileges.
|
||||
* tags:
|
||||
* - Config
|
||||
* operationId: getConfig
|
||||
@@ -24,39 +24,14 @@
|
||||
* properties:
|
||||
* item:
|
||||
* $ref: '#/components/schemas/Config'
|
||||
* security: []
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
async fn() {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const oidcClient = await sails.hooks.oidc.getClient();
|
||||
|
||||
let oidc = null;
|
||||
if (oidcClient) {
|
||||
const authorizationUrlParams = {
|
||||
scope: sails.config.custom.oidcScopes,
|
||||
};
|
||||
|
||||
if (!sails.config.custom.oidcUseDefaultResponseMode) {
|
||||
authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode;
|
||||
}
|
||||
|
||||
oidc = {
|
||||
authorizationUrl: oidcClient.authorizationUrl(authorizationUrlParams),
|
||||
endSessionUrl: oidcClient.issuer.end_session_endpoint ? oidcClient.endSessionUrl({}) : null,
|
||||
isEnforced: sails.config.custom.oidcEnforced,
|
||||
};
|
||||
}
|
||||
const config = await Config.qm.getOneMain();
|
||||
|
||||
return {
|
||||
item: sails.helpers.config.presentOne(
|
||||
{
|
||||
oidc,
|
||||
},
|
||||
currentUser,
|
||||
),
|
||||
item: sails.helpers.config.presentOne(config),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /config/test-smtp:
|
||||
* post:
|
||||
* summary: Test SMTP configuration
|
||||
* description: Sends a test email to verify the SMTP is configured correctly. Only available when SMTP is configured via the UI.
|
||||
* tags:
|
||||
* - Config
|
||||
* operationId: testSmtpConfig
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Test email sent successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - item
|
||||
* properties:
|
||||
* item:
|
||||
* $ref: '#/components/schemas/Config'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/Unauthorized'
|
||||
* 403:
|
||||
* $ref: '#/components/responses/Forbidden'
|
||||
*/
|
||||
|
||||
const Errors = {
|
||||
NOT_AVAILABLE: {
|
||||
notAvailable: 'Not available',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
exits: {
|
||||
notAvailable: {
|
||||
responseType: 'forbidden',
|
||||
},
|
||||
},
|
||||
|
||||
async fn() {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
if (sails.config.custom.smtpHost) {
|
||||
return Errors.NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
const { transporter, config } = await sails.helpers.utils.makeSmtpTransporter({
|
||||
connectionTimeout: 5000,
|
||||
greetingTimeout: 5000,
|
||||
socketTimeout: 10000,
|
||||
dnsTimeout: 3000,
|
||||
});
|
||||
|
||||
if (!transporter) {
|
||||
return Errors.NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
const logs = [];
|
||||
try {
|
||||
logs.push('📧 Sending test email...');
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const info = await transporter.sendMail({
|
||||
to: currentUser.email,
|
||||
subject: this.req.i18n.__('Test Title'),
|
||||
text: this.req.i18n.__('This is a test text message!'),
|
||||
html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'),
|
||||
});
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
logs.push('✅ Email sent successfully!', '');
|
||||
|
||||
logs.push(`📬 Message ID: ${info.messageId}`);
|
||||
if (info.response) {
|
||||
logs.push(`📤 Server response: ${info.response.trim()}`);
|
||||
}
|
||||
|
||||
logs.push('', '🎉 Your configuration is working correctly!');
|
||||
} catch (error) {
|
||||
logs.push('❌ Failed to send email!', '');
|
||||
|
||||
if (error.code) {
|
||||
logs.push(`⚠️ Error code: ${error.code}`);
|
||||
}
|
||||
logs.push(`💬 Reason: ${error.message.trim()}`);
|
||||
|
||||
if (error.code === 'EDNS') {
|
||||
logs.push('', '💡 Hint: Check your host setting.');
|
||||
} else if (error.code === 'ETIMEDOUT') {
|
||||
logs.push('', '💡 Hint: Check your host and port settings.');
|
||||
} else if (error.code === 'EAUTH') {
|
||||
logs.push('', '💡 Hint: Check your username and password.');
|
||||
} else if (error.code === 'ESOCKET') {
|
||||
if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) {
|
||||
logs.push('', '💡 Hint: Check your host and port settings.');
|
||||
} else if (error.message.includes('wrong version number')) {
|
||||
logs.push('', '💡 Hint: Try toggling "Use secure connection".');
|
||||
} else if (error.message.includes('certificate')) {
|
||||
logs.push('', '💡 Hint: Try toggling "Reject unauthorized TLS certificates".');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
transporter.close();
|
||||
}
|
||||
|
||||
return {
|
||||
item: sails.helpers.config.presentOne(config),
|
||||
included: {
|
||||
logs,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /config:
|
||||
* patch:
|
||||
* summary: Update application configuration
|
||||
* description: Updates the application configuration. Requires admin privileges.
|
||||
* tags:
|
||||
* - Config
|
||||
* operationId: updateConfig
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* smtpHost:
|
||||
* type: string
|
||||
* maxLength: 256
|
||||
* nullable: true
|
||||
* description: Hostname or IP address of the SMTP server
|
||||
* example: smtp.example.com
|
||||
* smtpHost:
|
||||
* type: number
|
||||
* minimum: 0
|
||||
* maximum: 65535
|
||||
* nullable: true
|
||||
* description: Port number of the SMTP server
|
||||
* example: 587
|
||||
* smtpName:
|
||||
* type: string
|
||||
* maxLength: 256
|
||||
* nullable: true
|
||||
* description: Client hostname used in the EHLO command for SMTP
|
||||
* example: localhost
|
||||
* smtpSecure:
|
||||
* type: boolean
|
||||
* description: Whether to use a secure connection for SMTP
|
||||
* example: false
|
||||
* smtpTlsRejectUnauthorized:
|
||||
* type: boolean
|
||||
* description: Whether to reject unauthorized or self-signed TLS certificates for SMTP connections
|
||||
* example: true
|
||||
* smtpUser:
|
||||
* type: string
|
||||
* maxLength: 256
|
||||
* nullable: true
|
||||
* description: Username for authenticating with the SMTP server
|
||||
* example: no-reply@example.com
|
||||
* smtpPassword:
|
||||
* type: string
|
||||
* maxLength: 256
|
||||
* nullable: true
|
||||
* description: Password for authenticating with the SMTP server
|
||||
* example: SecurePassword123!
|
||||
* smtpFrom:
|
||||
* type: string
|
||||
* maxLength: 256
|
||||
* nullable: true
|
||||
* description: Default "from" used for outgoing SMTP emails
|
||||
* example: no-reply@example.com
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Configuration updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - item
|
||||
* properties:
|
||||
* item:
|
||||
* $ref: '#/components/schemas/Config'
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
smtpHost: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
maxLength: 256,
|
||||
allowNull: true,
|
||||
},
|
||||
smtpPort: {
|
||||
type: 'number',
|
||||
min: 0,
|
||||
max: 65535,
|
||||
allowNull: true,
|
||||
},
|
||||
smtpName: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
maxLength: 256,
|
||||
allowNull: true,
|
||||
},
|
||||
smtpSecure: {
|
||||
type: 'boolean',
|
||||
},
|
||||
smtpTlsRejectUnauthorized: {
|
||||
type: 'boolean',
|
||||
},
|
||||
smtpUser: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
maxLength: 256,
|
||||
allowNull: true,
|
||||
},
|
||||
smtpPassword: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
maxLength: 256,
|
||||
allowNull: true,
|
||||
},
|
||||
smtpFrom: {
|
||||
type: 'string',
|
||||
isNotEmptyString: true,
|
||||
maxLength: 256,
|
||||
allowNull: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { currentUser } = this.req;
|
||||
|
||||
const values = _.pick(inputs, [
|
||||
'smtpHost',
|
||||
'smtpPort',
|
||||
'smtpName',
|
||||
'smtpSecure',
|
||||
'smtpTlsRejectUnauthorized',
|
||||
'smtpUser',
|
||||
'smtpPassword',
|
||||
'smtpFrom',
|
||||
]);
|
||||
|
||||
const config = await sails.helpers.config.updateMain.with({
|
||||
values,
|
||||
actorUser: currentUser,
|
||||
request: this.req,
|
||||
});
|
||||
|
||||
return {
|
||||
item: sails.helpers.config.presentOne(config),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -84,10 +84,17 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
await sails.helpers.notificationServices.testOne.with({
|
||||
record: notificationService,
|
||||
i18n: this.req.i18n,
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
await sails.helpers.utils.sendNotifications.with({
|
||||
services: [_.pick(notificationService, ['url', 'format'])],
|
||||
title: this.req.i18n.__('Test Title'),
|
||||
bodyByFormat: {
|
||||
text: this.req.i18n.__('This is a test text message!'),
|
||||
markdown: this.req.i18n.__('This is a *test* **markdown** `message`!'),
|
||||
html: this.req.i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>!'),
|
||||
},
|
||||
});
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
|
||||
return {
|
||||
item: notificationService,
|
||||
|
||||
@@ -54,14 +54,12 @@
|
||||
* included:
|
||||
* type: object
|
||||
* required:
|
||||
* - accessTokens
|
||||
* - accessToken
|
||||
* properties:
|
||||
* accessTokens:
|
||||
* type: array
|
||||
* accessToken:
|
||||
* type: string
|
||||
* description: New acces tokens (when updating own password)
|
||||
* items:
|
||||
* type: string
|
||||
* example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ4...
|
||||
* example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ4...
|
||||
* 400:
|
||||
* $ref: '#/components/responses/ValidationError'
|
||||
* 401:
|
||||
@@ -180,7 +178,7 @@ module.exports = {
|
||||
return {
|
||||
item: sails.helpers.users.presentOne(user, currentUser),
|
||||
included: {
|
||||
accessTokens: [accessToken],
|
||||
accessToken,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,9 +154,9 @@ module.exports = {
|
||||
await sails.helpers.notifications.createOne.with({
|
||||
values: {
|
||||
action,
|
||||
userId: action.data.user.id,
|
||||
type: action.type,
|
||||
data: action.data,
|
||||
userId: action.data.user.id,
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
},
|
||||
@@ -179,24 +179,20 @@ module.exports = {
|
||||
|
||||
const notifiableUserIds = _.union(cardSubscriptionUserIds, boardSubscriptionUserIds);
|
||||
|
||||
await Promise.all(
|
||||
notifiableUserIds.map((userId) =>
|
||||
sails.helpers.notifications.createOne.with({
|
||||
values: {
|
||||
userId,
|
||||
action,
|
||||
type: action.type,
|
||||
data: action.data,
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
},
|
||||
project: inputs.project,
|
||||
board: inputs.board,
|
||||
list: inputs.list,
|
||||
webhooks: inputs.webhooks,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await sails.helpers.notifications.createMany.with({
|
||||
arrayOfValues: notifiableUserIds.map((userId) => ({
|
||||
userId,
|
||||
action,
|
||||
type: action.type,
|
||||
data: action.data,
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
})),
|
||||
project: inputs.project,
|
||||
board: inputs.board,
|
||||
list: inputs.list,
|
||||
webhooks: inputs.webhooks,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
sync: true,
|
||||
|
||||
inputs: {
|
||||
oidc: {
|
||||
type: 'ref',
|
||||
},
|
||||
user: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const data = {
|
||||
oidc: inputs.oidc,
|
||||
version: sails.config.custom.version,
|
||||
};
|
||||
if (inputs.user && inputs.user.role === User.Roles.ADMIN) {
|
||||
data.activeUsersLimit = sails.config.custom.activeUsersLimit;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
};
|
||||
@@ -124,29 +124,25 @@ module.exports = {
|
||||
boardSubscriptionUserIds,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
notifiableUserIds.map((userId) =>
|
||||
sails.helpers.notifications.createOne.with({
|
||||
webhooks,
|
||||
values: {
|
||||
userId,
|
||||
comment,
|
||||
type: mentionUserIdsSet.has(userId)
|
||||
? Notification.Types.MENTION_IN_COMMENT
|
||||
: Notification.Types.COMMENT_CARD,
|
||||
data: {
|
||||
card: _.pick(values.card, ['name']),
|
||||
text: comment.text,
|
||||
},
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
},
|
||||
project: inputs.project,
|
||||
board: inputs.board,
|
||||
list: inputs.list,
|
||||
}),
|
||||
),
|
||||
);
|
||||
await sails.helpers.notifications.createMany.with({
|
||||
webhooks,
|
||||
arrayOfValues: notifiableUserIds.map((userId) => ({
|
||||
userId,
|
||||
comment,
|
||||
type: mentionUserIdsSet.has(userId)
|
||||
? Notification.Types.MENTION_IN_COMMENT
|
||||
: Notification.Types.COMMENT_CARD,
|
||||
data: {
|
||||
card: _.pick(values.card, ['name']),
|
||||
text: comment.text,
|
||||
},
|
||||
creatorUser: values.user,
|
||||
card: values.card,
|
||||
})),
|
||||
project: inputs.project,
|
||||
board: inputs.board,
|
||||
list: inputs.list,
|
||||
});
|
||||
|
||||
if (values.user.subscribeToCardWhenCommenting) {
|
||||
let cardSubscription;
|
||||
|
||||
@@ -11,20 +11,17 @@ module.exports = {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
user: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
fn(inputs) {
|
||||
const data = {
|
||||
...inputs.record,
|
||||
version: sails.config.custom.version,
|
||||
};
|
||||
if (inputs.user && inputs.user.role === User.Roles.ADMIN) {
|
||||
data.activeUsersLimit = sails.config.custom.activeUsersLimit;
|
||||
if (sails.config.custom.smtpHost) {
|
||||
return _.omit(inputs.record, Config.SMTP_FIELD_NAMES);
|
||||
}
|
||||
|
||||
return data;
|
||||
if (inputs.record.smtpPassword) {
|
||||
return _.omit(inputs.record, 'smtpPassword');
|
||||
}
|
||||
|
||||
return inputs.record;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
values: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
},
|
||||
actorUser: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
request: {
|
||||
type: 'ref',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { values } = inputs;
|
||||
|
||||
const config = await Config.qm.updateOneMain(values);
|
||||
|
||||
const configRelatedUserIds = await sails.helpers.users.getAllIds(User.Roles.ADMIN);
|
||||
|
||||
configRelatedUserIds.forEach((userId) => {
|
||||
sails.sockets.broadcast(
|
||||
`user:${userId}`,
|
||||
'configUpdate',
|
||||
{
|
||||
item: sails.helpers.config.presentOne(config),
|
||||
},
|
||||
inputs.request,
|
||||
);
|
||||
});
|
||||
|
||||
const webhooks = await Webhook.qm.getAll();
|
||||
|
||||
// TODO: with prevData?
|
||||
sails.helpers.utils.sendWebhooks.with({
|
||||
webhooks,
|
||||
event: Webhook.Events.CONFIG_UPDATE,
|
||||
buildData: () => ({
|
||||
item: sails.helpers.config.presentOne(config),
|
||||
}),
|
||||
user: inputs.actorUser,
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
@@ -1,33 +0,0 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
record: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
i18n: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { i18n } = inputs;
|
||||
|
||||
await sails.helpers.utils.sendNotifications.with({
|
||||
services: [_.pick(inputs.record, ['url', 'format'])],
|
||||
title: i18n.__('Test Title'),
|
||||
bodyByFormat: {
|
||||
text: i18n.__('This is a test text message!'),
|
||||
markdown: i18n.__('This is a *test* **markdown** `message`!'),
|
||||
html: i18n.__('This is a <i>test</i> <b>html</b> <code>message</code>'),
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,358 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const escapeMarkdown = require('escape-markdown');
|
||||
const escapeHtml = require('escape-html');
|
||||
|
||||
const { mentionMarkupToText } = require('../../../utils/mentions');
|
||||
|
||||
const buildTitle = (notification, t) => {
|
||||
switch (notification.type) {
|
||||
case Notification.Types.MOVE_CARD:
|
||||
return t('Card Moved');
|
||||
case Notification.Types.COMMENT_CARD:
|
||||
return t('New Comment');
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
return t('You Were Added to Card');
|
||||
case Notification.Types.MENTION_IN_COMMENT:
|
||||
return t('You Were Mentioned in Comment');
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildBodyByFormat = (board, card, notification, actorUser, t) => {
|
||||
const markdownCardLink = `[${escapeMarkdown(card.name)}](${sails.config.custom.baseUrl}/cards/${card.id})`;
|
||||
const htmlCardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${escapeHtml(card.name)}</a>`;
|
||||
|
||||
switch (notification.type) {
|
||||
case Notification.Types.MOVE_CARD: {
|
||||
const fromListName = sails.helpers.lists.makeName(notification.data.fromList);
|
||||
const toListName = sails.helpers.lists.makeName(notification.data.toList);
|
||||
|
||||
return {
|
||||
text: t(
|
||||
'%s moved %s from %s to %s on %s',
|
||||
actorUser.name,
|
||||
card.name,
|
||||
fromListName,
|
||||
toListName,
|
||||
board.name,
|
||||
),
|
||||
markdown: t(
|
||||
'%s moved %s from %s to %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
`**${escapeMarkdown(fromListName)}**`,
|
||||
`**${escapeMarkdown(toListName)}**`,
|
||||
escapeMarkdown(board.name),
|
||||
),
|
||||
html: t(
|
||||
'%s moved %s from %s to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
`<b>${escapeHtml(fromListName)}</b>`,
|
||||
`<b>${escapeHtml(toListName)}</b>`,
|
||||
escapeHtml(board.name),
|
||||
),
|
||||
};
|
||||
}
|
||||
case Notification.Types.COMMENT_CARD: {
|
||||
const commentText = _.truncate(mentionMarkupToText(notification.data.text));
|
||||
|
||||
return {
|
||||
text: `${t(
|
||||
'%s left a new comment to %s on %s',
|
||||
actorUser.name,
|
||||
card.name,
|
||||
board.name,
|
||||
)}:\n${commentText}`,
|
||||
markdown: `${t(
|
||||
'%s left a new comment to %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
escapeMarkdown(board.name),
|
||||
)}:\n\n*${escapeMarkdown(commentText)}*`,
|
||||
html: `${t(
|
||||
'%s left a new comment to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
escapeHtml(board.name),
|
||||
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
|
||||
};
|
||||
}
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
return {
|
||||
text: t('%s added you to %s on %s', actorUser.name, card.name, board.name),
|
||||
markdown: t(
|
||||
'%s added you to %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
escapeMarkdown(board.name),
|
||||
),
|
||||
html: t(
|
||||
'%s added you to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
escapeHtml(board.name),
|
||||
),
|
||||
};
|
||||
case Notification.Types.MENTION_IN_COMMENT: {
|
||||
const commentText = _.truncate(mentionMarkupToText(notification.data.text));
|
||||
|
||||
return {
|
||||
text: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
actorUser.name,
|
||||
card.name,
|
||||
board.name,
|
||||
)}:\n${commentText}`,
|
||||
markdown: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeMarkdown(actorUser.name),
|
||||
markdownCardLink,
|
||||
escapeMarkdown(board.name),
|
||||
)}:\n\n*${escapeMarkdown(commentText)}*`,
|
||||
html: `${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
htmlCardLink,
|
||||
escapeHtml(board.name),
|
||||
)}:\n\n<i>${escapeHtml(commentText)}</i>`,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const buildAndSendNotifications = async (services, board, card, notification, actorUser, t) => {
|
||||
await sails.helpers.utils.sendNotifications(
|
||||
services,
|
||||
buildTitle(notification, t),
|
||||
buildBodyByFormat(board, card, notification, actorUser, t),
|
||||
);
|
||||
};
|
||||
|
||||
// TODO: use templates (views) to build html
|
||||
const buildEmail = (board, card, notification, actorUser, notifiableUser, t) => {
|
||||
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${escapeHtml(card.name)}</a>`;
|
||||
const boardLink = `<a href="${sails.config.custom.baseUrl}/boards/${board.id}">${escapeHtml(board.name)}</a>`;
|
||||
|
||||
let html;
|
||||
switch (notification.type) {
|
||||
case Notification.Types.MOVE_CARD: {
|
||||
const fromListName = sails.helpers.lists.makeName(notification.data.fromList);
|
||||
const toListName = sails.helpers.lists.makeName(notification.data.toList);
|
||||
|
||||
html = `<p>${t(
|
||||
'%s moved %s from %s to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
escapeHtml(fromListName),
|
||||
escapeHtml(toListName),
|
||||
boardLink,
|
||||
)}</p>`;
|
||||
|
||||
break;
|
||||
}
|
||||
case Notification.Types.COMMENT_CARD:
|
||||
html = `<p>${t(
|
||||
'%s left a new comment to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
html = `<p>${t(
|
||||
'%s added you to %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.MENTION_IN_COMMENT:
|
||||
html = `<p>${t(
|
||||
'%s mentioned you in %s on %s',
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
|
||||
|
||||
break;
|
||||
default:
|
||||
return null; // TODO: throw error?
|
||||
}
|
||||
|
||||
return {
|
||||
html,
|
||||
to: notifiableUser.email,
|
||||
subject: buildTitle(notification, t),
|
||||
};
|
||||
};
|
||||
|
||||
const sendEmails = async (transporter, emails) => {
|
||||
await Promise.all(
|
||||
emails.map((email) =>
|
||||
sails.helpers.utils.sendEmail.with({
|
||||
...email,
|
||||
transporter,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
transporter.close();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
arrayOfValues: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
project: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
board: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
webhooks: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const { arrayOfValues } = inputs;
|
||||
|
||||
const ids = await sails.helpers.utils.generateIds(arrayOfValues.length);
|
||||
const valuesById = {};
|
||||
|
||||
const notifications = await Notification.qm.create(
|
||||
arrayOfValues.map((values) => {
|
||||
const id = ids.shift();
|
||||
|
||||
const isCommentRelated =
|
||||
values.type === Notification.Types.COMMENT_CARD ||
|
||||
values.type === Notification.Types.MENTION_IN_COMMENT;
|
||||
|
||||
const nextValues = {
|
||||
...values,
|
||||
id,
|
||||
creatorUserId: values.creatorUser.id,
|
||||
boardId: values.card.boardId,
|
||||
cardId: values.card.id,
|
||||
};
|
||||
|
||||
if (isCommentRelated) {
|
||||
nextValues.commentId = values.comment.id;
|
||||
} else {
|
||||
nextValues.actionId = values.action.id;
|
||||
}
|
||||
|
||||
valuesById[id] = { ...nextValues }; // FIXME: hack
|
||||
return nextValues;
|
||||
}),
|
||||
);
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
const values = valuesById[notification.id];
|
||||
|
||||
sails.sockets.broadcast(`user:${notification.userId}`, 'notificationCreate', {
|
||||
item: notification,
|
||||
included: {
|
||||
users: [sails.helpers.users.presentOne(values.creatorUser, {})], // FIXME: hack
|
||||
},
|
||||
});
|
||||
|
||||
sails.helpers.utils.sendWebhooks.with({
|
||||
webhooks: inputs.webhooks,
|
||||
event: Webhook.Events.NOTIFICATION_CREATE,
|
||||
buildData: () => ({
|
||||
item: notification,
|
||||
included: {
|
||||
projects: [inputs.project],
|
||||
boards: [inputs.board],
|
||||
lists: [inputs.list],
|
||||
cards: [values.card],
|
||||
...(notification.commentId
|
||||
? {
|
||||
comments: [values.comment],
|
||||
}
|
||||
: {
|
||||
actions: [values.action],
|
||||
}),
|
||||
},
|
||||
}),
|
||||
user: values.creatorUser,
|
||||
});
|
||||
});
|
||||
|
||||
const notificationsByUserId = _.groupBy(notifications, 'userId');
|
||||
const userIds = Object.keys(notificationsByUserId);
|
||||
|
||||
const notificationServices = await NotificationService.qm.getByUserIds(userIds);
|
||||
const { transporter } = await sails.helpers.utils.makeSmtpTransporter();
|
||||
|
||||
if (notificationServices.length > 0 || transporter) {
|
||||
const users = await User.qm.getByIds(userIds);
|
||||
const userById = _.keyBy(users, 'id');
|
||||
|
||||
const notificationServicesByUserId = _.groupBy(notificationServices, 'userId');
|
||||
|
||||
Object.keys(notificationsByUserId).forEach(async (userId) => {
|
||||
const notifiableUser = userById[userId];
|
||||
const t = sails.helpers.utils.makeTranslator(notifiableUser.language);
|
||||
|
||||
const emails = notificationsByUserId[userId].flatMap((notification) => {
|
||||
const values = valuesById[notification.id];
|
||||
|
||||
if (notificationServicesByUserId[userId]) {
|
||||
const services = notificationServicesByUserId[userId].map((notificationService) =>
|
||||
_.pick(notificationService, ['url', 'format']),
|
||||
);
|
||||
|
||||
buildAndSendNotifications(
|
||||
services,
|
||||
inputs.board,
|
||||
values.card,
|
||||
notification,
|
||||
values.creatorUser,
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
if (transporter) {
|
||||
return buildEmail(
|
||||
inputs.board,
|
||||
values.card,
|
||||
notification,
|
||||
values.creatorUser,
|
||||
notifiableUser,
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
|
||||
if (emails.length > 0) {
|
||||
sendEmails(transporter, emails);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return notifications;
|
||||
},
|
||||
};
|
||||
@@ -137,7 +137,15 @@ const buildAndSendNotifications = async (services, board, card, notification, ac
|
||||
};
|
||||
|
||||
// TODO: use templates (views) to build html
|
||||
const buildAndSendEmail = async (board, card, notification, actorUser, notifiableUser, t) => {
|
||||
const buildAndSendEmail = async (
|
||||
transporter,
|
||||
board,
|
||||
card,
|
||||
notification,
|
||||
actorUser,
|
||||
notifiableUser,
|
||||
t,
|
||||
) => {
|
||||
const cardLink = `<a href="${sails.config.custom.baseUrl}/cards/${card.id}">${escapeHtml(card.name)}</a>`;
|
||||
const boardLink = `<a href="${sails.config.custom.baseUrl}/boards/${board.id}">${escapeHtml(board.name)}</a>`;
|
||||
|
||||
@@ -164,7 +172,7 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
|
||||
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
|
||||
|
||||
break;
|
||||
case Notification.Types.ADD_MEMBER_TO_CARD:
|
||||
@@ -182,7 +190,7 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
|
||||
escapeHtml(actorUser.name),
|
||||
cardLink,
|
||||
boardLink,
|
||||
)}</p><p>${escapeHtml(notification.data.text)}</p>`;
|
||||
)}</p><p>${escapeHtml(mentionMarkupToText(notification.data.text))}</p>`;
|
||||
|
||||
break;
|
||||
default:
|
||||
@@ -190,10 +198,13 @@ const buildAndSendEmail = async (board, card, notification, actorUser, notifiabl
|
||||
}
|
||||
|
||||
await sails.helpers.utils.sendEmail.with({
|
||||
transporter,
|
||||
html,
|
||||
to: notifiableUser.email,
|
||||
subject: buildTitle(notification, t),
|
||||
});
|
||||
|
||||
transporter.close();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -223,10 +234,6 @@ module.exports = {
|
||||
async fn(inputs) {
|
||||
const { values } = inputs;
|
||||
|
||||
if (values.user) {
|
||||
values.userId = values.user.id;
|
||||
}
|
||||
|
||||
const isCommentRelated =
|
||||
values.type === Notification.Types.COMMENT_CARD ||
|
||||
values.type === Notification.Types.MENTION_IN_COMMENT;
|
||||
@@ -274,9 +281,10 @@ module.exports = {
|
||||
});
|
||||
|
||||
const notificationServices = await NotificationService.qm.getByUserId(notification.userId);
|
||||
const { transporter } = await sails.helpers.utils.makeSmtpTransporter();
|
||||
|
||||
if (notificationServices.length > 0 || sails.hooks.smtp.isEnabled()) {
|
||||
const notifiableUser = values.user || (await User.qm.getOneById(notification.userId));
|
||||
if (notificationServices.length > 0 || transporter) {
|
||||
const notifiableUser = await User.qm.getOneById(notification.userId);
|
||||
const t = sails.helpers.utils.makeTranslator(notifiableUser.language);
|
||||
|
||||
if (notificationServices.length > 0) {
|
||||
@@ -294,8 +302,9 @@ module.exports = {
|
||||
);
|
||||
}
|
||||
|
||||
if (sails.hooks.smtp.isEnabled()) {
|
||||
if (transporter) {
|
||||
buildAndSendEmail(
|
||||
transporter,
|
||||
inputs.board,
|
||||
values.card,
|
||||
notification,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
defaultOptions: {
|
||||
type: 'json',
|
||||
},
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
let config;
|
||||
let sourceConfig;
|
||||
|
||||
if (sails.config.custom.smtpHost) {
|
||||
sourceConfig = sails.config.custom;
|
||||
} else {
|
||||
config = await Config.qm.getOneMain();
|
||||
|
||||
if (config.smtpHost) {
|
||||
sourceConfig = config;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceConfig) {
|
||||
return {
|
||||
config,
|
||||
transporter: null,
|
||||
};
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(
|
||||
{
|
||||
...inputs.defaultOptions,
|
||||
host: sourceConfig.smtpHost,
|
||||
port: sourceConfig.smtpPort,
|
||||
name: sourceConfig.smtpName,
|
||||
secure: sourceConfig.smtpSecure,
|
||||
auth: sourceConfig.smtpUser && {
|
||||
user: sourceConfig.smtpUser,
|
||||
pass: sourceConfig.smtpPassword,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: sourceConfig.smtpTlsRejectUnauthorized,
|
||||
},
|
||||
},
|
||||
{
|
||||
from: sourceConfig.smtpFrom,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
transporter,
|
||||
config,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
module.exports = {
|
||||
inputs: {
|
||||
transporter: {
|
||||
type: 'ref',
|
||||
required: true,
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
@@ -20,13 +24,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
async fn(inputs) {
|
||||
const transporter = sails.hooks.smtp.getTransporter(); // TODO: check if enabled?
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail({
|
||||
...inputs,
|
||||
from: sails.config.custom.smtpFrom,
|
||||
});
|
||||
const info = await inputs.transporter.sendMail(inputs);
|
||||
|
||||
sails.log.info(`Email sent: ${info.messageId}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -59,6 +59,28 @@ module.exports = function defineOidcHook(sails) {
|
||||
return client;
|
||||
},
|
||||
|
||||
async getBootstrap() {
|
||||
const instance = await this.getClient();
|
||||
|
||||
if (!instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const authorizationUrlParams = {
|
||||
scope: sails.config.custom.oidcScopes,
|
||||
};
|
||||
|
||||
if (!sails.config.custom.oidcUseDefaultResponseMode) {
|
||||
authorizationUrlParams.response_mode = sails.config.custom.oidcResponseMode;
|
||||
}
|
||||
|
||||
return {
|
||||
authorizationUrl: instance.authorizationUrl(authorizationUrlParams),
|
||||
endSessionUrl: instance.issuer.end_session_endpoint ? instance.endSessionUrl({}) : null,
|
||||
isEnforced: sails.config.custom.oidcEnforced,
|
||||
};
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
return !!sails.config.custom.oidcIssuer;
|
||||
},
|
||||
|
||||
@@ -9,6 +9,37 @@ const defaultFind = (criteria) => Notification.find(criteria).sort('id DESC');
|
||||
|
||||
/* Query methods */
|
||||
|
||||
const create = (arrayOfValues) =>
|
||||
sails.getDatastore().transaction(async (db) => {
|
||||
const notifications = await Notification.createEach(arrayOfValues).fetch().usingConnection(db);
|
||||
const userIds = sails.helpers.utils.mapRecords(notifications, 'userId', true, true);
|
||||
|
||||
if (userIds.length > 0) {
|
||||
const queryValues = [];
|
||||
const inValues = userIds.map((userId) => {
|
||||
queryValues.push(userId);
|
||||
return `$${queryValues.length}`;
|
||||
});
|
||||
|
||||
queryValues.push(LIMIT);
|
||||
|
||||
const query = `
|
||||
WITH exceeded_notification AS (
|
||||
SELECT id, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY id DESC) AS rank
|
||||
FROM notification
|
||||
WHERE user_id IN (${inValues.join(', ')}) AND is_read = FALSE
|
||||
)
|
||||
UPDATE notification
|
||||
SET is_read = TRUE
|
||||
WHERE id IN (SELECT id FROM exceeded_notification WHERE rank > $${queryValues.length})
|
||||
`;
|
||||
|
||||
await sails.sendNativeQuery(query, queryValues).usingConnection(db);
|
||||
}
|
||||
|
||||
return notifications;
|
||||
});
|
||||
|
||||
const createOne = (values) => {
|
||||
if (values.userId) {
|
||||
return sails.getDatastore().transaction(async (db) => {
|
||||
@@ -26,7 +57,7 @@ const createOne = (values) => {
|
||||
)
|
||||
UPDATE notification
|
||||
SET is_read = TRUE
|
||||
WHERE id in (SELECT id FROM exceeded_notification)
|
||||
WHERE id IN (SELECT id FROM exceeded_notification)
|
||||
`;
|
||||
|
||||
await sails.sendNativeQuery(query, [values.userId, LIMIT]).usingConnection(db);
|
||||
@@ -66,6 +97,7 @@ const updateOne = (criteria, values) => Notification.updateOne(criteria).set({ .
|
||||
const delete_ = (criteria) => Notification.destroy(criteria).fetch();
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
createOne,
|
||||
getByIds,
|
||||
getUnreadByUserId,
|
||||
|
||||
@@ -14,6 +14,11 @@ const getByUserId = (userId) =>
|
||||
userId,
|
||||
});
|
||||
|
||||
const getByUserIds = (userIds) =>
|
||||
defaultFind({
|
||||
userId: userIds,
|
||||
});
|
||||
|
||||
const getByBoardId = (boardId) =>
|
||||
defaultFind({
|
||||
boardId,
|
||||
@@ -36,6 +41,7 @@ const deleteOne = (criteria) => NotificationService.destroyOne(criteria);
|
||||
module.exports = {
|
||||
createOne,
|
||||
getByUserId,
|
||||
getByUserIds,
|
||||
getByBoardId,
|
||||
getByBoardIds,
|
||||
getOneById,
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
/*!
|
||||
* Copyright (c) 2024 PLANKA Software GmbH
|
||||
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
|
||||
*/
|
||||
|
||||
/**
|
||||
* smtp hook
|
||||
*
|
||||
* @description :: A hook definition. Extends Sails by adding shadow routes, implicit actions,
|
||||
* and/or initialization logic.
|
||||
* @docs :: https://sailsjs.com/docs/concepts/extending-sails/hooks
|
||||
*/
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
module.exports = function defineSmtpHook(sails) {
|
||||
let transporter = null;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Runs when this Sails app loads/lifts.
|
||||
*/
|
||||
|
||||
async initialize() {
|
||||
if (!this.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
sails.log.info('Initializing custom hook (`smtp`)');
|
||||
|
||||
transporter = nodemailer.createTransport({
|
||||
pool: true,
|
||||
host: sails.config.custom.smtpHost,
|
||||
port: sails.config.custom.smtpPort,
|
||||
name: sails.config.custom.smtpName,
|
||||
secure: sails.config.custom.smtpSecure,
|
||||
auth: sails.config.custom.smtpUser && {
|
||||
user: sails.config.custom.smtpUser,
|
||||
pass: sails.config.custom.smtpPassword,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: sails.config.custom.smtpTlsRejectUnauthorized,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
getTransporter() {
|
||||
return transporter;
|
||||
},
|
||||
|
||||
isEnabled() {
|
||||
return !!sails.config.custom.smtpHost;
|
||||
},
|
||||
};
|
||||
};
|
||||
+108
-31
@@ -17,54 +17,131 @@
|
||||
* Config:
|
||||
* type: object
|
||||
* required:
|
||||
* - version
|
||||
* - oidc
|
||||
* - id
|
||||
* - isInitialized
|
||||
* properties:
|
||||
* version:
|
||||
* id:
|
||||
* type: string
|
||||
* description: Current version of the PLANKA application
|
||||
* example: 2.0.0
|
||||
* activeUsersLimit:
|
||||
* description: Unique identifier for the config (always set to '1')
|
||||
* example: 1
|
||||
* smtpHost:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Hostname or IP address of the SMTP server
|
||||
* example: smtp.example.com
|
||||
* smtpPort:
|
||||
* type: number
|
||||
* nullable: true
|
||||
* description: Maximum number of active users allowed (conditionally added for admins if configured)
|
||||
* example: 100
|
||||
* oidc:
|
||||
* type: object
|
||||
* required:
|
||||
* - authorizationUrl
|
||||
* - endSessionUrl
|
||||
* - isEnforced
|
||||
* description: Port number of the SMTP server
|
||||
* example: 587
|
||||
* smtpName:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: OpenID Connect configuration (null if not configured)
|
||||
* properties:
|
||||
* authorizationUrl:
|
||||
* type: string
|
||||
* format: uri
|
||||
* description: OIDC authorization URL for initiating authentication
|
||||
* example: https://oidc.example.com/auth
|
||||
* endSessionUrl:
|
||||
* type: string
|
||||
* format: uri
|
||||
* nullable: true
|
||||
* description: OIDC end session URL for logout (null if not supported by provider)
|
||||
* example: https://oidc.example.com/logout
|
||||
* isEnforced:
|
||||
* type: boolean
|
||||
* description: Whether OIDC authentication is enforced (users must use OIDC to login)
|
||||
* example: false
|
||||
* description: Client hostname used in the EHLO command for SMTP
|
||||
* example: localhost
|
||||
* smtpSecure:
|
||||
* type: boolean
|
||||
* description: Whether to use a secure connection for SMTP
|
||||
* example: false
|
||||
* smtpTlsRejectUnauthorized:
|
||||
* type: boolean
|
||||
* description: Whether to reject unauthorized or self-signed TLS certificates for SMTP connections
|
||||
* example: true
|
||||
* smtpUser:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Username for authenticating with the SMTP server
|
||||
* example: no-reply@example.com
|
||||
* smtpPassword:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Password for authenticating with the SMTP server
|
||||
* example: SecurePassword123!
|
||||
* smtpFrom:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* description: Default "from" used for outgoing SMTP emails
|
||||
* example: no-reply@example.com
|
||||
* isInitialized:
|
||||
* type: boolean
|
||||
* description: Whether the PLANKA instance has been initialized
|
||||
* example: true
|
||||
* createdAt:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* nullable: true
|
||||
* description: When the config was created
|
||||
* example: 2024-01-01T00:00:00.000Z
|
||||
* updatedAt:
|
||||
* type: string
|
||||
* format: date-time
|
||||
* nullable: true
|
||||
* description: When the config was last updated
|
||||
* example: 2024-01-01T00:00:00.000Z
|
||||
*/
|
||||
|
||||
const MAIN_ID = '1';
|
||||
|
||||
const SMTP_FIELD_NAMES = [
|
||||
'smtpHost',
|
||||
'smtpPort',
|
||||
'smtpName',
|
||||
'smtpSecure',
|
||||
'smtpTlsRejectUnauthorized',
|
||||
'smtpUser',
|
||||
'smtpPassword',
|
||||
'smtpFrom',
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
MAIN_ID,
|
||||
SMTP_FIELD_NAMES,
|
||||
|
||||
attributes: {
|
||||
// ╔═╗╦═╗╦╔╦╗╦╔╦╗╦╦ ╦╔═╗╔═╗
|
||||
// ╠═╝╠╦╝║║║║║ ║ ║╚╗╔╝║╣ ╚═╗
|
||||
// ╩ ╩╚═╩╩ ╩╩ ╩ ╩ ╚╝ ╚═╝╚═╝
|
||||
|
||||
smtpHost: {
|
||||
type: 'string',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_host',
|
||||
},
|
||||
smtpPort: {
|
||||
type: 'number',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_port',
|
||||
},
|
||||
smtpName: {
|
||||
type: 'string',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_name',
|
||||
},
|
||||
smtpSecure: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
columnName: 'smtp_secure',
|
||||
},
|
||||
smtpTlsRejectUnauthorized: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
columnName: 'smtp_tls_reject_unauthorized',
|
||||
},
|
||||
smtpUser: {
|
||||
type: 'string',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_user',
|
||||
},
|
||||
smtpPassword: {
|
||||
type: 'string',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_password',
|
||||
},
|
||||
smtpFrom: {
|
||||
type: 'string',
|
||||
allowNull: true,
|
||||
columnName: 'smtp_from',
|
||||
},
|
||||
isInitialized: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
|
||||
@@ -107,6 +107,8 @@ const Events = {
|
||||
COMMENT_UPDATE: 'commentUpdate',
|
||||
COMMENT_DELETE: 'commentDelete',
|
||||
|
||||
CONFIG_UPDATE: 'configUpdate',
|
||||
|
||||
CUSTOM_FIELD_CREATE: 'customFieldCreate',
|
||||
CUSTOM_FIELD_UPDATE: 'customFieldUpdate',
|
||||
CUSTOM_FIELD_DELETE: 'customFieldDelete',
|
||||
|
||||
Reference in New Issue
Block a user