diff --git a/ui/config-overrides.js b/ui/config-overrides.js index 354453e4..438c1c29 100644 --- a/ui/config-overrides.js +++ b/ui/config-overrides.js @@ -25,8 +25,8 @@ module.exports = { const config = configFunction(proxy, allowedHost); config.proxy = { '/answer': { - // target: "http://10.0.20.84:8080", - target: 'http://10.0.10.98:2060', + target: "http://10.0.20.84:8080", + // target: 'http://10.0.10.98:2060', changeOrigin: true, secure: false, }, diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 9f7f6f5e..3e577515 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -53,6 +53,6 @@ export const ADMIN_NAV_MENUS = [ }, { name: 'settings', - child: [{ name: 'general' }, { name: 'interface' }], + child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }], }, ]; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 73eac5d4..6558a97c 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -264,10 +264,23 @@ export interface AdminSettingsInterface { theme: string; } +export interface AdminSettingsSmtp { + encryption: string; + from_email: string; + from_name: string; + smtp_authentication: boolean; + smtp_host: string; + smtp_password: string; + smtp_port: number; + smtp_username: string; + test_email_recipient?: string; +} + export interface SiteSettings { general: AdminSettingsGeneral; interface: AdminSettingsInterface; } + /** * @description interface for Activity */ diff --git a/ui/src/common/pattern.ts b/ui/src/common/pattern.ts new file mode 100644 index 00000000..406e5e3e --- /dev/null +++ b/ui/src/common/pattern.ts @@ -0,0 +1,6 @@ +const pattern = { + email: + /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+\.)+[a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]{2,}))$/, +}; + +export default pattern; diff --git a/ui/src/i18n/locales/en.json b/ui/src/i18n/locales/en.json index 5754df75..81e34926 100644 --- a/ui/src/i18n/locales/en.json +++ b/ui/src/i18n/locales/en.json @@ -734,7 +734,8 @@ "flags": "Flags", "settings": "Settings", "general": "General", - "interface": "Interface" + "interface": "Interface", + "smtp": "SMTP" }, "dashboard": { "title": "Dashboard", @@ -857,6 +858,55 @@ "msg": "Interface language cannot be empty.", "text": "User interface language. It will change when you refresh the page." } + }, + "smtp": { + "page_title": "SMTP", + "from_email": { + "label": "From Email", + "msg": "From email cannot be empty.", + "text": "The email address which emails are sent from." + }, + "from_name": { + "label": "From Name", + "msg": "From name cannot be empty.", + "text": "The name which emails are sent from." + }, + "smtp_host": { + "label": "SMTP Host", + "msg": "SMTP host cannot be empty.", + "text": "Your mail server." + }, + "encryption": { + "label": "Encryption", + "msg": "Encryption cannot be empty.", + "text": "For most servers SSL is the recommended option.", + "ssl": "SSL", + "none": "None" + }, + "smtp_port": { + "label": "SMTP Port", + "msg": "SMTP port must be number 1 ~ 65535.", + "text": "The port to your mail server." + }, + "smtp_username": { + "label": "SMTP Username", + "msg": "SMTP username cannot be empty." + }, + "smtp_password": { + "label": "SMTP Password", + "msg": "SMTP password cannot be empty." + }, + "test_email_recipient": { + "label": "Test Email Recipients", + "text": "Provide email address that will receive test sends.", + "msg": "Test email recipients is invalid" + }, + "smtp_authentication": { + "label": "SMTP Authentication", + "msg": "SMTP authentication cannot be empty.", + "yes": "Yes", + "no": "No" + } } } } diff --git a/ui/src/pages/Admin/Interface/index.tsx b/ui/src/pages/Admin/Interface/index.tsx index 66f7ef4c..5a332d20 100644 --- a/ui/src/pages/Admin/Interface/index.tsx +++ b/ui/src/pages/Admin/Interface/index.tsx @@ -18,8 +18,6 @@ import { import { interfaceStore } from '@answer/stores'; import { UploadImg } from '@answer/components'; -import '../index.scss'; - const Interface: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'admin.interface', diff --git a/ui/src/pages/Admin/Smtp/index.tsx b/ui/src/pages/Admin/Smtp/index.tsx new file mode 100644 index 00000000..6d993909 --- /dev/null +++ b/ui/src/pages/Admin/Smtp/index.tsx @@ -0,0 +1,322 @@ +import React, { FC, useEffect, useState } from 'react'; +import { Form, Button, Stack } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; + +import type * as Type from '@answer/common/interface'; +import { useToast } from '@answer/hooks'; +import { useSmtpSetting, updateSmtpSetting } from '@answer/api'; + +import pattern from '@/common/pattern'; + +const Smtp: FC = () => { + const { t } = useTranslation('translation', { + keyPrefix: 'admin.smtp', + }); + const Toast = useToast(); + const { data: setting } = useSmtpSetting(); + const [formData, setFormData] = useState({ + from_email: { + value: '', + isInvalid: false, + errorMsg: '', + }, + from_name: { + value: '', + isInvalid: false, + errorMsg: '', + }, + smtp_host: { + value: '', + isInvalid: false, + errorMsg: '', + }, + encryption: { + value: '', + isInvalid: false, + errorMsg: '', + }, + smtp_port: { + value: '', + isInvalid: false, + errorMsg: '', + }, + smtp_authentication: { + value: 'yes', + isInvalid: false, + errorMsg: '', + }, + smtp_username: { + value: '', + isInvalid: false, + errorMsg: '', + }, + smtp_password: { + value: '', + isInvalid: false, + errorMsg: '', + }, + test_email_recipient: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + const checkValidated = (): boolean => { + let ret = true; + const { smtp_port, test_email_recipient } = formData; + if ( + !/^[1-9][0-9]*$/.test(smtp_port.value) || + Number(smtp_port.value) > 65535 + ) { + ret = false; + formData.smtp_port = { + value: smtp_port.value, + isInvalid: true, + errorMsg: t('smtp_port.msg'), + }; + } + if ( + test_email_recipient.value && + !pattern.email.test(test_email_recipient.value) + ) { + ret = false; + formData.test_email_recipient = { + value: test_email_recipient.value, + isInvalid: true, + errorMsg: t('test_email_recipient.msg'), + }; + } + setFormData({ + ...formData, + }); + return ret; + }; + + const onSubmit = (evt) => { + evt.preventDefault(); + evt.stopPropagation(); + if (!checkValidated()) { + return; + } + const reqParams: Type.AdminSettingsSmtp = { + from_email: formData.from_email.value, + from_name: formData.from_name.value, + smtp_host: formData.smtp_host.value, + encryption: formData.encryption.value, + smtp_port: Number(formData.smtp_port.value), + smtp_authentication: formData.smtp_authentication.value, + smtp_username: formData.smtp_username.value, + smtp_password: formData.smtp_password.value, + test_email_recipient: formData.test_email_recipient.value, + }; + + updateSmtpSetting(reqParams) + .then(() => { + Toast.onShow({ + msg: t('update', { keyPrefix: 'toast' }), + variant: 'success', + }); + }) + .catch((err) => { + if (err.isError && err.key) { + formData[err.key].isInvalid = true; + formData[err.key].errorMsg = err.value; + } + setFormData({ ...formData }); + }); + }; + const onFieldChange = (fieldName, fieldValue) => { + if (!formData[fieldName]) { + return; + } + const fieldData: Type.FormDataType = { + [fieldName]: { + value: fieldValue, + isInvalid: false, + errorMsg: '', + }, + }; + setFormData({ ...formData, ...fieldData }); + }; + useEffect(() => { + if (!setting) { + return; + } + const formState = {}; + Object.keys(formData).forEach((k) => { + let v = setting[k]; + if (v === null || v === undefined) { + v = ''; + } + formState[k] = { ...formData[k], value: v }; + }); + setFormData(formState); + }, [setting]); + + return ( + <> +

{t('page_title')}

+
+ + {t('from_email.label')} + onFieldChange('from_email', evt.target.value)} + /> + {t('from_email.text')} + + {formData.from_email.errorMsg} + + + + {t('from_name.label')} + onFieldChange('from_name', evt.target.value)} + /> + {t('from_name.text')} + + {formData.from_name.errorMsg} + + + + {t('smtp_host.label')} + onFieldChange('smtp_host', evt.target.value)} + /> + {t('smtp_host.text')} + + {formData.smtp_host.errorMsg} + + + + {t('encryption.label')} + + onFieldChange('encryption', 'SSL')} + type="radio" + /> + onFieldChange('encryption', '')} + type="radio" + /> + + {t('encryption.text')} + + {formData.encryption.errorMsg} + + + + {t('smtp_port.label')} + onFieldChange('smtp_port', evt.target.value)} + /> + {t('smtp_port.text')} + + {formData.smtp_port.errorMsg} + + + + {t('smtp_authentication.label')} + + onFieldChange('smtp_authentication', true)} + type="radio" + /> + onFieldChange('smtp_authentication', false)} + type="radio" + /> + + + {formData.smtp_authentication.errorMsg} + + + + {t('smtp_username.label')} + onFieldChange('smtp_username', evt.target.value)} + /> + {t('smtp_username.text')} + + {formData.smtp_username.errorMsg} + + + + {t('smtp_password.label')} + onFieldChange('smtp_password', evt.target.value)} + /> + {t('smtp_password.text')} + + {formData.smtp_password.errorMsg} + + + + {t('test_email_recipient.label')} + + onFieldChange('test_email_recipient', evt.target.value) + } + /> + {t('test_email_recipient.text')} + + {formData.test_email_recipient.errorMsg} + + + + +
+ + ); +}; + +export default Smtp; diff --git a/ui/src/pages/Admin/index.scss b/ui/src/pages/Admin/index.scss index 7eac3dbc..5874432f 100644 --- a/ui/src/pages/Admin/index.scss +++ b/ui/src/pages/Admin/index.scss @@ -1,7 +1,3 @@ -.fluid-bg-container { - height: 3rem; -} - .admin-container { padding-top: 2rem; padding-bottom: 2rem; diff --git a/ui/src/router/route-config.ts b/ui/src/router/route-config.ts index 6267482e..b942464f 100644 --- a/ui/src/router/route-config.ts +++ b/ui/src/router/route-config.ts @@ -179,6 +179,10 @@ const routeConfig: RouteNode[] = [ path: 'users/:user_id', page: 'pages/Admin/UserOverview', }, + { + path: 'smtp', + page: 'pages/Admin/Smtp', + }, ], }, { diff --git a/ui/src/services/admin/settings.ts b/ui/src/services/admin/settings.ts index ca7950ff..e1f486e7 100644 --- a/ui/src/services/admin/settings.ts +++ b/ui/src/services/admin/settings.ts @@ -52,3 +52,21 @@ export const updateInterfaceSetting = (params: Type.AdminSettingsInterface) => { const apiUrl = `/answer/admin/api/siteinfo/interface`; return request.put(apiUrl, params); }; + +export const useSmtpSetting = () => { + const apiUrl = `/answer/admin/api/setting/smtp`; + const { data, error } = useSWR( + [apiUrl], + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + }; +}; + +export const updateSmtpSetting = (params: Type.AdminSettingsSmtp) => { + const apiUrl = `/answer/admin/api/setting/smtp`; + return request.put(apiUrl, params); +};