mirror of https://github.com/openspug/spug
# 优化推送集成
parent
06d6bb93cf
commit
2efb1b88f9
|
@ -6,7 +6,7 @@ from django.conf import settings
|
|||
from libs.mixins import AdminView, View
|
||||
from libs import JsonParser, Argument, human_datetime, json_response
|
||||
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.setting.utils import AppSetting
|
||||
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})
|
||||
if mfa['enable']:
|
||||
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)
|
||||
send_login_wx_code(user.wx_token, code)
|
||||
send_login_code(spug_push_key, user.wx_token, code)
|
||||
cache.set(key, code, 300)
|
||||
return json_response({'required_mfa': True})
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@ from django.conf import settings
|
|||
from libs import JsonParser, Argument, json_response, auth
|
||||
from libs.utils import generate_random_str
|
||||
from libs.mail import Mail
|
||||
from libs.spug import send_login_wx_code
|
||||
from libs.push import get_balance
|
||||
from libs.push import get_balance, send_login_code
|
||||
from libs.mixins import AdminView
|
||||
from apps.setting.utils import AppSetting
|
||||
from apps.setting.models import Setting, KEYS_DEFAULT
|
||||
|
@ -41,9 +40,12 @@ class MFAView(AdminView):
|
|||
def get(self, request):
|
||||
if not request.user.wx_token:
|
||||
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)
|
||||
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)
|
||||
return json_response()
|
||||
|
||||
|
@ -107,17 +109,6 @@ def email_test(request):
|
|||
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')
|
||||
def get_about(request):
|
||||
return json_response({
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright: (c) OpenSpug Organization. https://github.com/openspug/spug
|
||||
# Copyright: (c) <spug.dev@gmail.com>
|
||||
# Released under the AGPL-3.0 License.
|
||||
from apps.setting.utils import AppSetting
|
||||
import requests
|
||||
|
||||
push_server = 'https://push.spug.cc'
|
||||
|
@ -24,3 +25,21 @@ def get_contacts(token):
|
|||
return res['data']
|
||||
except Exception:
|
||||
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'])
|
||||
|
|
|
@ -7,34 +7,9 @@ from apps.notify.models import Notify
|
|||
from libs.mail import Mail
|
||||
from libs.utils import human_datetime
|
||||
from libs.push import push_server
|
||||
from functools import partial
|
||||
import requests
|
||||
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:
|
||||
def __init__(self, grp, event, target, title, message, duration):
|
||||
|
@ -44,7 +19,6 @@ class Notification:
|
|||
self.target = target
|
||||
self.message = message
|
||||
self.duration = duration
|
||||
self.spug_key = AppSetting.get_default('spug_key')
|
||||
self.spug_push_key = AppSetting.get_default('spug_push_key')
|
||||
|
||||
@staticmethod
|
||||
|
@ -72,20 +46,6 @@ class Notification:
|
|||
raise NotImplementedError
|
||||
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):
|
||||
mail_service = AppSetting.get_default('mail_service', {})
|
||||
body = [
|
||||
|
@ -101,17 +61,11 @@ class Notification:
|
|||
subject = f'{event_map[self.event]}-{self.title}'
|
||||
mail = Mail(**mail_service)
|
||||
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:
|
||||
make_no_spug_key_notify()
|
||||
Notify.make_monitor_notify(
|
||||
'发送报警信息失败',
|
||||
'未配置报警服务,请在系统管理/系统设置/报警服务设置中配置邮件服务。'
|
||||
)
|
||||
|
||||
def monitor_by_dd(self, users):
|
||||
texts = [
|
||||
|
@ -158,7 +112,10 @@ class Notification:
|
|||
|
||||
def monitor_by_spug_push(self, targets):
|
||||
if not self.spug_push_key:
|
||||
make_no_push_key_notify()
|
||||
Notify.make_monitor_notify(
|
||||
'发送报警信息失败',
|
||||
'未绑定推送服务,请在系统管理/系统设置/推送服务设置中绑定推送助手账户。'
|
||||
)
|
||||
return
|
||||
data = {
|
||||
'source': 'monitor',
|
||||
|
@ -188,15 +145,6 @@ class Notification:
|
|||
if mode == '1':
|
||||
wx_mp_ids = set(x for x in push_ids if x.startswith('wx_mp_'))
|
||||
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':
|
||||
sms_ids = set(x for x in push_ids if x.startswith('sms_'))
|
||||
targets.update(sms_ids)
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Modal, Form, Select, Input, message } from 'antd';
|
||||
import http from 'libs/http';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {observer} from 'mobx-react';
|
||||
import {Modal, Form, Select, Input, message} from 'antd';
|
||||
import {http, includes} from 'libs';
|
||||
import store from './store';
|
||||
import rStore from '../role/store';
|
||||
|
||||
|
@ -15,6 +15,12 @@ import rStore from '../role/store';
|
|||
export default observer(function () {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [contacts, setContacts] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
http.get('/api/alarm/contact/?only_push=1')
|
||||
.then(res => setContacts(res))
|
||||
}, []);
|
||||
|
||||
function handleSubmit() {
|
||||
setLoading(true);
|
||||
|
@ -44,11 +50,13 @@ export default observer(function () {
|
|||
<Form.Item required name="nickname" label="姓名">
|
||||
<Input placeholder="请输入姓名"/>
|
||||
</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="请输入密码"/>
|
||||
</Form.Item>
|
||||
<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="请选择">
|
||||
{rStore.records.map(item => (
|
||||
<Select.Option value={item.id} key={item.id}>{item.name}</Select.Option>
|
||||
|
@ -61,13 +69,17 @@ export default observer(function () {
|
|||
</Form.Item>
|
||||
<Form.Item
|
||||
name="wx_token"
|
||||
label="微信Token"
|
||||
label="推送标识"
|
||||
extra={(
|
||||
<span>
|
||||
如果启用了MFA(两步验证)则该项为必填。
|
||||
<a target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/wx-token/">什么是微信Token?</a>
|
||||
</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>
|
||||
</Modal>
|
||||
|
|
|
@ -3,17 +3,14 @@
|
|||
* Copyright (c) <spug.dev@gmail.com>
|
||||
* Released under the AGPL-3.0 License.
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Form, Switch, Input, Space, message, Button } from 'antd';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import {observer} from 'mobx-react';
|
||||
import {Form, Switch, Input, Space, Spin, message, Button} from 'antd';
|
||||
import styles from './index.module.css';
|
||||
import http from 'libs/http';
|
||||
import store from './store';
|
||||
|
||||
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 [visible, setVisible] = useState(false);
|
||||
const [counter, setCounter] = useState(0);
|
||||
|
@ -29,25 +26,25 @@ export default observer(function () {
|
|||
}, [counter])
|
||||
|
||||
function handleChangeVerifyIP(v) {
|
||||
setVerifyIP(v);
|
||||
store.isFetching = true;
|
||||
http.post('/api/setting/', {data: [{key: 'verify_ip', value: v}]})
|
||||
.then(() => {
|
||||
message.success('设置成功');
|
||||
store.fetchSettings()
|
||||
})
|
||||
}, () => store.isFetching = false)
|
||||
}
|
||||
|
||||
function handleChangeBindIP(v) {
|
||||
setBindIP(v);
|
||||
store.isFetching = true;
|
||||
http.post('/api/setting/', {data: [{key: 'bind_ip', value: v}]})
|
||||
.then(() => {
|
||||
message.success('设置成功');
|
||||
store.fetchSettings()
|
||||
})
|
||||
}, () => store.isFetching = false)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -62,7 +59,6 @@ export default observer(function () {
|
|||
setLoading2(true)
|
||||
http.post('/api/setting/mfa/', {enable: v, code})
|
||||
.then(() => {
|
||||
setMFA({enable: v});
|
||||
setVisible(false);
|
||||
message.success('设置成功');
|
||||
store.fetchSettings()
|
||||
|
@ -70,8 +66,9 @@ export default observer(function () {
|
|||
.finally(() => setLoading2(false))
|
||||
}
|
||||
|
||||
const {verify_ip, bind_ip, MFA} = store.settings;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Spin spinning={store.isFetching}>
|
||||
<div className={styles.title}>安全设置</div>
|
||||
<Form layout="vertical" style={{maxWidth: 500}}>
|
||||
<Form.Item
|
||||
|
@ -98,8 +95,9 @@ export default observer(function () {
|
|||
label="登录MFA(两步)认证"
|
||||
style={{marginTop: 24}}
|
||||
extra={visible ? '输入验证码,通过验证后开启。' :
|
||||
<span>建议开启,登录时额外使用验证码进行身份验证。开启前至少要确保管理员账户配置了微信Token(账户管理/编辑),开启后未配置微信Token的账户将无法登录,<a
|
||||
target="_blank" rel="noopener noreferrer" href="https://spug.cc/docs/wx-token/">什么是微信Token?</a></span>}>
|
||||
<span>建议开启,登录时额外使用验证码进行身份验证。开启前至少要确保管理员账户配置了推送标识(账户管理/编辑),开启后未配置的账户将无法登录,<a
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
href="https://spug.cc/docs/wx-token/">什么是微信Token?</a></span>}>
|
||||
{visible ? (
|
||||
<div style={{display: 'flex', width: 490}}>
|
||||
<Form.Item noStyle extra="验证通过后开启MFA(两步验证)。">
|
||||
|
@ -120,10 +118,10 @@ export default observer(function () {
|
|||
checkedChildren="开启"
|
||||
unCheckedChildren="关闭"
|
||||
onChange={handleChangeMFA}
|
||||
checked={mfa.enable}/>
|
||||
checked={MFA?.enable}/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</React.Fragment>
|
||||
</Spin>
|
||||
)
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue