mirror of
https://github.com/plankanban/planka.git
synced 2025-12-23 01:11:40 +03:00
feat: Add ability to configure and test SMTP via UI
This commit is contained in:
72
server/api/controllers/bootstrap/show.js
Normal file
72
server/api/controllers/bootstrap/show.js
Normal file
@@ -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),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
117
server/api/controllers/config/test-smtp.js
Normal file
117
server/api/controllers/config/test-smtp.js
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
151
server/api/controllers/config/update.js
Normal file
151
server/api/controllers/config/update.js
Normal file
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user