feat(admin): add SMTP setting for admin

This commit is contained in:
haitao(lj) 2022-10-21 17:17:33 +08:00
parent 1ba76183fe
commit e9b5be9daf
10 changed files with 417 additions and 10 deletions

View File

@ -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,
},

View File

@ -53,6 +53,6 @@ export const ADMIN_NAV_MENUS = [
},
{
name: 'settings',
child: [{ name: 'general' }, { name: 'interface' }],
child: [{ name: 'general' }, { name: 'interface' }, { name: 'smtp' }],
},
];

View File

@ -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
*/

6
ui/src/common/pattern.ts Normal file
View File

@ -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;

View File

@ -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"
}
}
}
}

View File

@ -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',

View File

@ -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<Type.FormDataType>({
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 (
<>
<h3 className="mb-4">{t('page_title')}</h3>
<Form noValidate onSubmit={onSubmit}>
<Form.Group controlId="fromEmail" className="mb-3">
<Form.Label>{t('from_email.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.from_email.value}
isInvalid={formData.from_email.isInvalid}
onChange={(evt) => onFieldChange('from_email', evt.target.value)}
/>
<Form.Text as="div">{t('from_email.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.from_email.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="fromName" className="mb-3">
<Form.Label>{t('from_name.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.from_name.value}
isInvalid={formData.from_name.isInvalid}
onChange={(evt) => onFieldChange('from_name', evt.target.value)}
/>
<Form.Text as="div">{t('from_name.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.from_name.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="smtpHost" className="mb-3">
<Form.Label>{t('smtp_host.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.smtp_host.value}
isInvalid={formData.smtp_host.isInvalid}
onChange={(evt) => onFieldChange('smtp_host', evt.target.value)}
/>
<Form.Text as="div">{t('smtp_host.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.smtp_host.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="encryption" className="mb-3">
<Form.Label>{t('encryption.label')}</Form.Label>
<Stack direction="horizontal">
<Form.Check
inline
label={t('encryption.ssl')}
name="smtp_encryption"
id="smtp_encryption_ssl"
checked={formData.encryption.value === 'SSL'}
onChange={() => onFieldChange('encryption', 'SSL')}
type="radio"
/>
<Form.Check
inline
label={t('encryption.none')}
name="smtp_encryption"
id="smtp_encryption_none"
checked={!formData.encryption.value}
onChange={() => onFieldChange('encryption', '')}
type="radio"
/>
</Stack>
<Form.Text as="div">{t('encryption.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.encryption.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="smtpPort" className="mb-3">
<Form.Label>{t('smtp_port.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.smtp_port.value}
isInvalid={formData.smtp_port.isInvalid}
onChange={(evt) => onFieldChange('smtp_port', evt.target.value)}
/>
<Form.Text as="div">{t('smtp_port.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.smtp_port.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="smtpAuthentication" className="mb-3">
<Form.Label>{t('smtp_authentication.label')}</Form.Label>
<Stack direction="horizontal">
<Form.Check
inline
label={t('smtp_authentication.yes')}
name="smtp_authentication"
id="smtp_authentication_yes"
checked={!!formData.smtp_authentication.value}
onChange={() => onFieldChange('smtp_authentication', true)}
type="radio"
/>
<Form.Check
inline
label={t('smtp_authentication.no')}
name="smtp_authentication"
id="smtp_authentication_no"
checked={!formData.smtp_authentication.value}
onChange={() => onFieldChange('smtp_authentication', false)}
type="radio"
/>
</Stack>
<Form.Control.Feedback type="invalid">
{formData.smtp_authentication.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="smtpUsername" className="mb-3">
<Form.Label>{t('smtp_username.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.smtp_username.value}
isInvalid={formData.smtp_username.isInvalid}
onChange={(evt) => onFieldChange('smtp_username', evt.target.value)}
/>
<Form.Text as="div">{t('smtp_username.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.smtp_username.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="smtpPassword" className="mb-3">
<Form.Label>{t('smtp_password.label')}</Form.Label>
<Form.Control
required
type="password"
value={formData.smtp_password.value}
isInvalid={formData.smtp_password.isInvalid}
onChange={(evt) => onFieldChange('smtp_password', evt.target.value)}
/>
<Form.Text as="div">{t('smtp_password.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.smtp_password.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Form.Group controlId="testEmailRecipient" className="mb-3">
<Form.Label>{t('test_email_recipient.label')}</Form.Label>
<Form.Control
required
type="text"
value={formData.test_email_recipient.value}
isInvalid={formData.test_email_recipient.isInvalid}
onChange={(evt) =>
onFieldChange('test_email_recipient', evt.target.value)
}
/>
<Form.Text as="div">{t('test_email_recipient.text')}</Form.Text>
<Form.Control.Feedback type="invalid">
{formData.test_email_recipient.errorMsg}
</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" type="submit">
{t('save', { keyPrefix: 'btns' })}
</Button>
</Form>
</>
);
};
export default Smtp;

View File

@ -1,7 +1,3 @@
.fluid-bg-container {
height: 3rem;
}
.admin-container {
padding-top: 2rem;
padding-bottom: 2rem;

View File

@ -179,6 +179,10 @@ const routeConfig: RouteNode[] = [
path: 'users/:user_id',
page: 'pages/Admin/UserOverview',
},
{
path: 'smtp',
page: 'pages/Admin/Smtp',
},
],
},
{

View File

@ -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<Type.AdminSettingsSmtp, Error>(
[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);
};