feat: Add ability to configure and test SMTP via UI

This commit is contained in:
Maksim Eltyshev
2025-09-22 20:35:13 +02:00
parent 3a12bb7457
commit c6f4dcdb70
114 changed files with 2161 additions and 301 deletions
+3 -3
View File
@@ -21,14 +21,14 @@ export default function* coreSaga() {
yield take(ActionTypes.LOGOUT);
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcConfig && oidcConfig.endSessionUrl !== null) {
if (oidcBootstrap && oidcBootstrap.endSessionUrl !== null) {
const currentUser = yield select(selectors.selectCurrentUser);
if (!currentUser || currentUser.isSsoUser) {
// Redirect the user to the IDP to log out.
window.location.href = oidcConfig.endSessionUrl;
window.location.href = oidcBootstrap.endSessionUrl;
return;
}
}
+4
View File
@@ -18,8 +18,11 @@ export function* fetchCore() {
included: { notificationServices: notificationServices1 },
} = yield call(request, api.getCurrentUser, true);
let config;
let webhooks;
if (user.role === UserRoles.ADMIN) {
({ item: config } = yield call(request, api.getConfig));
({ items: webhooks } = yield call(request, api.getWebhooks));
}
@@ -105,6 +108,7 @@ export function* fetchCore() {
}
return {
config,
user,
board,
webhooks,
+49
View File
@@ -0,0 +1,49 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { call, put } from 'redux-saga/effects';
import request from '../request';
import actions from '../../../actions';
import api from '../../../api';
export function* updateConfig(data) {
yield put(actions.updateConfig(data));
let config;
try {
({ item: config } = yield call(request, api.updateConfig, data));
} catch (error) {
yield put(actions.updateConfig.failure(error));
return;
}
yield put(actions.updateConfig.success(config));
}
export function* handleConfigUpdate(config) {
yield put(actions.handleConfigUpdate(config));
}
export function* testSmtpConfig() {
yield put(actions.testSmtpConfig());
let logs;
try {
({
included: { logs },
} = yield call(request, api.testSmtpConfig));
} catch (error) {
yield put(actions.testSmtpConfig.failure(error));
}
yield put(actions.testSmtpConfig.success(logs));
}
export default {
updateConfig,
handleConfigUpdate,
testSmtpConfig,
};
+4 -2
View File
@@ -14,11 +14,12 @@ import i18n from '../../../i18n';
import { removeAccessToken } from '../../../utils/access-token-storage';
export function* initializeCore() {
const { item: config } = yield call(request, api.getConfig); // TODO: handle error
const { item: bootstrap } = yield call(request, api.getBootstrap); // TODO: handle error
yield put(actions.initializeCore.fetchConfig(config));
yield put(actions.initializeCore.fetchBootstrap(bootstrap));
const {
config,
user,
board,
webhooks,
@@ -49,6 +50,7 @@ export function* initializeCore() {
yield put(
actions.initializeCore(
config,
user,
board,
webhooks,
+2
View File
@@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -34,6 +35,7 @@ export default {
...socket,
...core,
...modals,
...config,
...webhooks,
...users,
...projects,
+3 -1
View File
@@ -21,6 +21,7 @@ export function* handleSocketReconnect() {
yield put(actions.handleSocketReconnect.fetchCore(currentUserId, boardId));
let bootstrap;
let config;
let user;
let board;
@@ -47,7 +48,7 @@ export function* handleSocketReconnect() {
let notificationServices;
try {
({ item: config } = yield call(request, api.getConfig));
({ item: bootstrap } = yield call(request, api.getBootstrap));
({
user,
@@ -80,6 +81,7 @@ export function* handleSocketReconnect() {
yield put(
actions.handleSocketReconnect(
bootstrap,
config,
user,
board,
+5 -4
View File
@@ -68,6 +68,7 @@ export function* handleUserUpdate(user) {
const currentUser = yield select(selectors.selectCurrentUser);
const isCurrentUser = user.id === currentUser.id;
let bootstrap;
let config;
let board;
let webhooks;
@@ -102,6 +103,7 @@ export function* handleUserUpdate(user) {
({ items: users1 } = yield call(request, api.getUsers));
if (user.role === UserRoles.ADMIN) {
({ item: bootstrap } = yield call(request, api.getBootstrap));
({ item: config } = yield call(request, api.getConfig));
({ items: webhooks } = yield call(request, api.getWebhooks));
@@ -164,6 +166,7 @@ export function* handleUserUpdate(user) {
user,
projectIds,
boardIds,
bootstrap,
config,
board,
webhooks,
@@ -248,10 +251,10 @@ export function* updateUserPassword(id, data) {
yield put(actions.updateUserPassword(id, data));
let user;
let accessTokens;
let accessToken;
try {
({ item: user, included: { accessTokens } = {} } = yield call(
({ item: user, included: { accessToken } = {} } = yield call(
request,
api.updateUserPassword,
id,
@@ -262,8 +265,6 @@ export function* updateUserPassword(id, data) {
return;
}
const accessToken = accessTokens && accessTokens[0];
if (accessToken) {
yield call(setAccessToken, accessToken);
}
+21
View File
@@ -0,0 +1,21 @@
/*!
* Copyright (c) 2024 PLANKA Software GmbH
* Licensed under the Fair Use License: https://github.com/plankanban/planka/blob/master/LICENSE.md
*/
import { all, takeEvery } from 'redux-saga/effects';
import services from '../services';
import EntryActionTypes from '../../../constants/EntryActionTypes';
export default function* configWatchers() {
yield all([
takeEvery(EntryActionTypes.CONFIG_UPDATE, ({ payload: { data } }) =>
services.updateConfig(data),
),
takeEvery(EntryActionTypes.CONFIG_UPDATE_HANDLE, ({ payload: { config } }) =>
services.handleConfigUpdate(config),
),
takeEvery(EntryActionTypes.SMTP_CONFIG_TEST, () => services.testSmtpConfig()),
]);
}
+2
View File
@@ -7,6 +7,7 @@ import router from './router';
import socket from './socket';
import core from './core';
import modals from './modals';
import config from './config';
import webhooks from './webhooks';
import users from './users';
import projects from './projects';
@@ -34,6 +35,7 @@ export default [
socket,
core,
modals,
config,
webhooks,
users,
projects,
+20
View File
@@ -29,6 +29,18 @@ const createSocketEventsChannel = () =>
emit(entryActions.handleConfigUpdate(item));
};
const handleWebhookCreate = ({ item }) => {
emit(entryActions.handleWebhookCreate(item));
};
const handleWebhookUpdate = ({ item }) => {
emit(entryActions.handleWebhookUpdate(item));
};
const handleWebhookDelete = ({ item }) => {
emit(entryActions.handleWebhookDelete(item));
};
const handleUserCreate = ({ item }) => {
emit(entryActions.handleUserCreate(item));
};
@@ -280,6 +292,10 @@ const createSocketEventsChannel = () =>
socket.on('configUpdate', handleConfigUpdate);
socket.on('webhookCreate', handleWebhookCreate);
socket.on('webhookUpdate', handleWebhookUpdate);
socket.on('webhookDelete', handleWebhookDelete);
socket.on('userCreate', handleUserCreate);
socket.on('userUpdate', handleUserUpdate);
socket.on('userDelete', handleUserDelete);
@@ -370,6 +386,10 @@ const createSocketEventsChannel = () =>
socket.off('configUpdate', handleConfigUpdate);
socket.off('webhookCreate', handleWebhookCreate);
socket.off('webhookUpdate', handleWebhookUpdate);
socket.off('webhookDelete', handleWebhookDelete);
socket.off('userCreate', handleUserCreate);
socket.off('userUpdate', handleUserUpdate);
socket.off('userDelete', handleUserDelete);
+4 -4
View File
@@ -16,9 +16,9 @@ import Paths from '../../../constants/Paths';
import AccessTokenSteps from '../../../constants/AccessTokenSteps';
export function* initializeLogin() {
const { item: config } = yield call(api.getConfig); // TODO: handle error
const { item: bootstrap } = yield call(api.getBootstrap); // TODO: handle error
yield put(actions.initializeLogin(config));
yield put(actions.initializeLogin(bootstrap));
}
export function* authenticate(data) {
@@ -42,7 +42,7 @@ export function* authenticate(data) {
}
export function* authenticateWithOidc() {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
const state = nanoid();
window.localStorage.setItem('oidc-state', state);
@@ -50,7 +50,7 @@ export function* authenticateWithOidc() {
const nonce = nanoid();
window.localStorage.setItem('oidc-nonce', nonce);
let redirectUrl = `${oidcConfig.authorizationUrl}`;
let redirectUrl = `${oidcBootstrap.authorizationUrl}`;
redirectUrl += `&state=${encodeURIComponent(state)}`;
redirectUrl += `&nonce=${encodeURIComponent(nonce)}`;
+2 -2
View File
@@ -49,9 +49,9 @@ export function* handleLocationChange() {
switch (pathsMatch.pattern.path) {
case Paths.LOGIN: {
const oidcConfig = yield select(selectors.selectOidcConfig);
const oidcBootstrap = yield select(selectors.selectOidcBootstrap);
if (oidcConfig) {
if (oidcBootstrap) {
const params = new URLSearchParams(window.location.search);
if (params.has('authenticateWithOidc')) {