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