# 优化推送集成

dependabot/pip/spug_api/paramiko-3.4.0
vapao 2023-11-13 16:59:02 +08:00
parent 06d6bb93cf
commit 2efb1b88f9
6 changed files with 75 additions and 104 deletions

View File

@ -6,7 +6,7 @@ from django.conf import settings
from libs.mixins import AdminView, View from libs.mixins import AdminView, View
from libs import JsonParser, Argument, human_datetime, json_response from libs import JsonParser, Argument, human_datetime, json_response
from libs.utils import get_request_real_ip, generate_random_str from libs.utils import get_request_real_ip, generate_random_str
from libs.spug import send_login_wx_code from libs.push import send_login_code
from apps.account.models import User, Role, History from apps.account.models import User, Role, History
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from apps.account.utils import verify_password from apps.account.utils import verify_password
@ -255,9 +255,12 @@ def handle_user_info(handle_response, request, user, captcha):
mfa = AppSetting.get_default('MFA', {'enable': False}) mfa = AppSetting.get_default('MFA', {'enable': False})
if mfa['enable']: if mfa['enable']:
if not user.wx_token: if not user.wx_token:
return handle_response(error='已启用登录双重认证但您的账户未配置微信Token请联系管理员') return handle_response(error='已启用登录双重认证,但您的账户未配置推送标识,请联系管理员')
spug_push_key = AppSetting.get_default('spug_push_key')
if not spug_push_key:
return handle_response(error='已启用登录双重认证,但系统未配置推送服务,请联系管理员')
code = generate_random_str(6) code = generate_random_str(6)
send_login_wx_code(user.wx_token, code) send_login_code(spug_push_key, user.wx_token, code)
cache.set(key, code, 300) cache.set(key, code, 300)
return json_response({'required_mfa': True}) return json_response({'required_mfa': True})

View File

@ -7,8 +7,7 @@ from django.conf import settings
from libs import JsonParser, Argument, json_response, auth from libs import JsonParser, Argument, json_response, auth
from libs.utils import generate_random_str from libs.utils import generate_random_str
from libs.mail import Mail from libs.mail import Mail
from libs.spug import send_login_wx_code from libs.push import get_balance, send_login_code
from libs.push import get_balance
from libs.mixins import AdminView from libs.mixins import AdminView
from apps.setting.utils import AppSetting from apps.setting.utils import AppSetting
from apps.setting.models import Setting, KEYS_DEFAULT from apps.setting.models import Setting, KEYS_DEFAULT
@ -41,9 +40,12 @@ class MFAView(AdminView):
def get(self, request): def get(self, request):
if not request.user.wx_token: if not request.user.wx_token:
return json_response( return json_response(
error='检测到当前账户未配置微信Token请配置后再尝试启用MFA认证否则可能造成系统无法正常登录。') error='检测到当前账户未配置推送标识(账户管理/编辑请配置后再尝试启用MFA认证否则可能造成系统无法正常登录。')
spug_push_key = AppSetting.get_default('spug_push_key')
if not spug_push_key:
return json_response(error='检测到当前账户未绑定推送服务,请在系统设置/推送服务设置中绑定推送助手账户。')
code = generate_random_str(6) code = generate_random_str(6)
send_login_wx_code(request.user.wx_token, code) send_login_code(spug_push_key, request.user.wx_token, code)
cache.set(f'{request.user.username}:code', code, 300) cache.set(f'{request.user.username}:code', code, 300)
return json_response() return json_response()
@ -107,17 +109,6 @@ def email_test(request):
return json_response(error=error) return json_response(error=error)
@auth('admin')
def mfa_test(request):
if not request.user.wx_token:
return json_response(
error='检测到当前账户未配置微信Token请配置后再尝试启用MFA认证否则可能造成系统无法正常登录。')
code = generate_random_str(6)
send_login_wx_code(request.user.wx_token, code)
cache.set(f'{request.user.username}:code', code, 300)
return json_response()
@auth('admin') @auth('admin')
def get_about(request): def get_about(request):
return json_response({ return json_response({

View File

@ -1,6 +1,7 @@
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug # Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
# Copyright: (c) <spug.dev@gmail.com> # Copyright: (c) <spug.dev@gmail.com>
# Released under the AGPL-3.0 License. # Released under the AGPL-3.0 License.
from apps.setting.utils import AppSetting
import requests import requests
push_server = 'https://push.spug.cc' push_server = 'https://push.spug.cc'
@ -24,3 +25,21 @@ def get_contacts(token):
return res['data'] return res['data']
except Exception: except Exception:
return [] return []
def send_login_code(token, user, code):
url = f'{push_server}/spug/message/'
data = {
'token': token,
'targets': [user],
'source': 'mfa',
'dataset': {
'code': code
}
}
res = requests.post(url, json=data, timeout=15)
if res.status_code != 200:
raise Exception(f'status code: {res.status_code}')
res = res.json()
if res.get('error'):
raise Exception(res['error'])

View File

@ -7,34 +7,9 @@ from apps.notify.models import Notify
from libs.mail import Mail from libs.mail import Mail
from libs.utils import human_datetime from libs.utils import human_datetime
from libs.push import push_server from libs.push import push_server
from functools import partial
import requests import requests
import json import json
spug_server = 'https://api.spug.cc'
notify_source = 'monitor'
make_no_spug_key_notify = partial(
Notify.make_monitor_notify,
'发送报警信息失败',
'未配置报警服务调用凭据,请在系统管理/系统设置/基本设置/调用凭据中配置。'
)
make_no_push_key_notify = partial(
Notify.make_monitor_notify,
'发送报警信息失败',
'未绑定推送服务,请在系统管理/系统设置/推送服务设置中绑定推送助手账户。'
)
def send_login_wx_code(wx_token, code):
url = f'{spug_server}/apis/login/wx/'
spug_key = AppSetting.get_default('spug_key')
res = requests.post(url, json={'token': spug_key, 'user': wx_token, 'code': code}, timeout=30)
if res.status_code != 200:
raise Exception(f'status code: {res.status_code}')
res = res.json()
if res.get('error'):
raise Exception(res['error'])
class Notification: class Notification:
def __init__(self, grp, event, target, title, message, duration): def __init__(self, grp, event, target, title, message, duration):
@ -44,7 +19,6 @@ class Notification:
self.target = target self.target = target
self.message = message self.message = message
self.duration = duration self.duration = duration
self.spug_key = AppSetting.get_default('spug_key')
self.spug_push_key = AppSetting.get_default('spug_push_key') self.spug_push_key = AppSetting.get_default('spug_push_key')
@staticmethod @staticmethod
@ -72,20 +46,6 @@ class Notification:
raise NotImplementedError raise NotImplementedError
Notify.make_system_notify('通知发送失败', f'返回数据:{res}') Notify.make_system_notify('通知发送失败', f'返回数据:{res}')
def monitor_by_wx(self, users):
if not self.spug_key:
make_no_spug_key_notify()
return
data = {
'token': self.spug_key,
'event': self.event,
'subject': f'{self.title} >> {self.target}',
'desc': self.message,
'remark': f'故障持续{self.duration}' if self.event == '2' else None,
'users': list(users)
}
self.handle_request(f'{spug_server}/apis/notify/wx/', data, 'spug')
def monitor_by_email(self, users): def monitor_by_email(self, users):
mail_service = AppSetting.get_default('mail_service', {}) mail_service = AppSetting.get_default('mail_service', {})
body = [ body = [
@ -101,17 +61,11 @@ class Notification:
subject = f'{event_map[self.event]}-{self.title}' subject = f'{event_map[self.event]}-{self.title}'
mail = Mail(**mail_service) mail = Mail(**mail_service)
mail.send_text_mail(users, subject, '\r\n'.join(body) + '\r\n\r\n自动发送,请勿回复。') mail.send_text_mail(users, subject, '\r\n'.join(body) + '\r\n\r\n自动发送,请勿回复。')
elif self.spug_key:
data = {
'token': self.spug_key,
'event': self.event,
'subject': self.title,
'body': '\r\n'.join(body),
'users': list(users)
}
self.handle_request(f'{spug_server}/apis/notify/mail/', data, 'spug')
else: else:
make_no_spug_key_notify() Notify.make_monitor_notify(
'发送报警信息失败',
'未配置报警服务,请在系统管理/系统设置/报警服务设置中配置邮件服务。'
)
def monitor_by_dd(self, users): def monitor_by_dd(self, users):
texts = [ texts = [
@ -158,7 +112,10 @@ class Notification:
def monitor_by_spug_push(self, targets): def monitor_by_spug_push(self, targets):
if not self.spug_push_key: if not self.spug_push_key:
make_no_push_key_notify() Notify.make_monitor_notify(
'发送报警信息失败',
'未绑定推送服务,请在系统管理/系统设置/推送服务设置中绑定推送助手账户。'
)
return return
data = { data = {
'source': 'monitor', 'source': 'monitor',
@ -188,15 +145,6 @@ class Notification:
if mode == '1': if mode == '1':
wx_mp_ids = set(x for x in push_ids if x.startswith('wx_mp_')) wx_mp_ids = set(x for x in push_ids if x.startswith('wx_mp_'))
targets.update(wx_mp_ids) targets.update(wx_mp_ids)
users = set(x.wx_token for x in Contact.objects.filter(id__in=u_ids, wx_token__isnull=False))
if not users:
if not wx_mp_ids:
Notify.make_monitor_notify(
'发送报警信息失败',
'未找到可用的通知对象请确保设置了相关报警联系人的微信Token。'
)
continue
self.monitor_by_wx(users)
elif mode == '2': elif mode == '2':
sms_ids = set(x for x in push_ids if x.startswith('sms_')) sms_ids = set(x for x in push_ids if x.startswith('sms_'))
targets.update(sms_ids) targets.update(sms_ids)

View File

@ -3,11 +3,11 @@
* Copyright (c) <spug.dev@gmail.com> * Copyright (c) <spug.dev@gmail.com>
* Released under the AGPL-3.0 License. * Released under the AGPL-3.0 License.
*/ */
import React, { useState } from 'react'; import React, {useState, useEffect} from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import {Modal, Form, Select, Input, message} from 'antd'; import {Modal, Form, Select, Input, message} from 'antd';
import http from 'libs/http'; import {http, includes} from 'libs';
import store from './store'; import store from './store';
import rStore from '../role/store'; import rStore from '../role/store';
@ -15,6 +15,12 @@ import rStore from '../role/store';
export default observer(function () { export default observer(function () {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [contacts, setContacts] = useState([])
useEffect(() => {
http.get('/api/alarm/contact/?only_push=1')
.then(res => setContacts(res))
}, []);
function handleSubmit() { function handleSubmit() {
setLoading(true); setLoading(true);
@ -44,11 +50,13 @@ export default observer(function () {
<Form.Item required name="nickname" label="姓名"> <Form.Item required name="nickname" label="姓名">
<Input placeholder="请输入姓名"/> <Input placeholder="请输入姓名"/>
</Form.Item> </Form.Item>
<Form.Item required hidden={store.record.id} name="password" label="密码" extra="至少8位包含数字、小写和大写字母。"> <Form.Item required hidden={store.record.id} name="password" label="密码"
extra="至少8位包含数字、小写和大写字母。">
<Input.Password placeholder="请输入密码"/> <Input.Password placeholder="请输入密码"/>
</Form.Item> </Form.Item>
<Form.Item hidden={store.record.is_supper} label="角色" style={{marginBottom: 0}}> <Form.Item hidden={store.record.is_supper} label="角色" style={{marginBottom: 0}}>
<Form.Item name="role_ids" style={{display: 'inline-block', width: '80%'}} extra="权限最大化原则,组合多个角色权限。"> <Form.Item name="role_ids" style={{display: 'inline-block', width: '80%'}}
extra="权限最大化原则,组合多个角色权限。">
<Select mode="multiple" placeholder="请选择"> <Select mode="multiple" placeholder="请选择">
{rStore.records.map(item => ( {rStore.records.map(item => (
<Select.Option value={item.id} key={item.id}>{item.name}</Select.Option> <Select.Option value={item.id} key={item.id}>{item.name}</Select.Option>
@ -61,13 +69,17 @@ export default observer(function () {
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="wx_token" name="wx_token"
label="微信Token" label="推送标识"
extra={( extra={(
<span> <span>
如果启用了MFA两步验证则该项为必填 如果启用了MFA两步验证则该项为必填
<a target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/wx-token/">什么是微信Token</a> <a target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/wx-token/">什么是微信Token</a>
</span>)}> </span>)}>
<Input placeholder="请输入微信Token"/> <Select showSearch filterOption={(i, o) => includes(o.children, i)} placeholder="请选择绑定推送标识">
{contacts.map(item => (
<Select.Option value={item.id} key={item.id}>{item.name}</Select.Option>
))}
</Select>
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@ -5,15 +5,12 @@
*/ */
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect} from 'react';
import {observer} from 'mobx-react'; import {observer} from 'mobx-react';
import { Form, Switch, Input, Space, message, Button } from 'antd'; import {Form, Switch, Input, Space, Spin, message, Button} from 'antd';
import styles from './index.module.css'; import styles from './index.module.css';
import http from 'libs/http'; import http from 'libs/http';
import store from './store'; import store from './store';
export default observer(function () { export default observer(function () {
const [verify_ip, setVerifyIP] = useState(store.settings.verify_ip);
const [bind_ip, setBindIP] = useState(store.settings.bind_ip);
const [mfa, setMFA] = useState(store.settings.MFA || {});
const [code, setCode] = useState(); const [code, setCode] = useState();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [counter, setCounter] = useState(0); const [counter, setCounter] = useState(0);
@ -29,25 +26,25 @@ export default observer(function () {
}, [counter]) }, [counter])
function handleChangeVerifyIP(v) { function handleChangeVerifyIP(v) {
setVerifyIP(v); store.isFetching = true;
http.post('/api/setting/', {data: [{key: 'verify_ip', value: v}]}) http.post('/api/setting/', {data: [{key: 'verify_ip', value: v}]})
.then(() => { .then(() => {
message.success('设置成功'); message.success('设置成功');
store.fetchSettings() store.fetchSettings()
}) }, () => store.isFetching = false)
} }
function handleChangeBindIP(v) { function handleChangeBindIP(v) {
setBindIP(v); store.isFetching = true;
http.post('/api/setting/', {data: [{key: 'bind_ip', value: v}]}) http.post('/api/setting/', {data: [{key: 'bind_ip', value: v}]})
.then(() => { .then(() => {
message.success('设置成功'); message.success('设置成功');
store.fetchSettings() store.fetchSettings()
}) }, () => store.isFetching = false)
} }
function handleChangeMFA(v) { function handleChangeMFA(v) {
if (v && !store.settings.spug_key) return message.error('开启MFA认证需要先在基本设置中配置调用凭据'); if (v && !store.settings.spug_push_key) return message.error('开启MFA认证需要先在推送服务设置中绑定推送助手账户');
v ? setVisible(true) : handleMFAModify(false) v ? setVisible(true) : handleMFAModify(false)
} }
@ -62,7 +59,6 @@ export default observer(function () {
setLoading2(true) setLoading2(true)
http.post('/api/setting/mfa/', {enable: v, code}) http.post('/api/setting/mfa/', {enable: v, code})
.then(() => { .then(() => {
setMFA({enable: v});
setVisible(false); setVisible(false);
message.success('设置成功'); message.success('设置成功');
store.fetchSettings() store.fetchSettings()
@ -70,8 +66,9 @@ export default observer(function () {
.finally(() => setLoading2(false)) .finally(() => setLoading2(false))
} }
const {verify_ip, bind_ip, MFA} = store.settings;
return ( return (
<React.Fragment> <Spin spinning={store.isFetching}>
<div className={styles.title}>安全设置</div> <div className={styles.title}>安全设置</div>
<Form layout="vertical" style={{maxWidth: 500}}> <Form layout="vertical" style={{maxWidth: 500}}>
<Form.Item <Form.Item
@ -98,8 +95,9 @@ export default observer(function () {
label="登录MFA两步认证" label="登录MFA两步认证"
style={{marginTop: 24}} style={{marginTop: 24}}
extra={visible ? '输入验证码,通过验证后开启。' : extra={visible ? '输入验证码,通过验证后开启。' :
<span>建议开启登录时额外使用验证码进行身份验证开启前至少要确保管理员账户配置了微信Token账户管理/编辑开启后未配置微信Token的账户将无法登录<a <span>建议开启登录时额外使用验证码进行身份验证开启前至少要确保管理员账户配置了推送标识账户管理/编辑开启后未配置的账户将无法登录<a
target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/wx-token/">什么是微信Token</a></span>}> target="_blank" rel="noopener noreferrer"
href="https://spug.cc/docs/wx-token/">什么是微信Token</a></span>}>
{visible ? ( {visible ? (
<div style={{display: 'flex', width: 490}}> <div style={{display: 'flex', width: 490}}>
<Form.Item noStyle extra="验证通过后开启MFA两步验证。"> <Form.Item noStyle extra="验证通过后开启MFA两步验证。">
@ -120,10 +118,10 @@ export default observer(function () {
checkedChildren="开启" checkedChildren="开启"
unCheckedChildren="关闭" unCheckedChildren="关闭"
onChange={handleChangeMFA} onChange={handleChangeMFA}
checked={mfa.enable}/> checked={MFA?.enable}/>
)} )}
</Form.Item> </Form.Item>
</Form> </Form>
</React.Fragment> </Spin>
) )
}) })