Merge branch 'feature/update_to_layui' of https://gitee.com/yadeno/ad-password-self-service into gitee_master

# Conflicts:
#	.gitignore
#	LICENSE
This commit is contained in:
Leven 2023-01-18 09:34:30 +08:00
commit 263cb7ce6f
93 changed files with 342308 additions and 21 deletions

30
.gitignore vendored
View File

@ -1,18 +1,12 @@
# Build and Release Folders
bin-debug/
bin-release/
[Oo]bj/
[Bb]in/
# Other files and folders
.settings/
# Executables
*.swf
*.air
*.ipa
*.apk
# Project files, i.e. `.project`, `.actionScriptProperties` and `.flexProperties`
# should NOT be excluded as they contain compiler settings and other important
# information for Eclipse / Flash Builder.
/.idea
.idea
jsLibraryMappings.xml
misc.xml
modules.xml
pwdselfservice.iml
remote-mappings.xml
workspace.xml
codeStyles
inspectionProfiles
deployment.xml
encodings.xml

View File

@ -1,4 +1,4 @@
Apache License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [Xiangle] [Xiangle]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -198,4 +198,4 @@
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.

264
auto-install.sh Normal file
View File

@ -0,0 +1,264 @@
#!/bin/bash
echo -e "此脚本为快速部署目前只做了Centos版本的如果是其它系统请自行修改下相关命令\n请准备一个新的环境运行\n本脚本会快速安装相关的环境和所需要的服务\n如果你运行脚本的服务器中已经存在如Nginx、Python3等可能会破坏掉原有的应用配置。"
##Check IP
function check_ip() {
local IP=$1
VALID_CHECK=$(echo $IP|awk -F. '$1<=255&&$2<=255&&$3<=255&&$4<=255{print "yes"}')
if echo $IP|grep -E "^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$" >/dev/null; then
if [[ $VALID_CHECK == "yes" ]]; then
return 0
else
return 1
fi
else
return 1
fi
}
##Check domain
function check_domain() {
local DOMAIN=$1
if echo $DOMAIN |grep -P "(?=^.{4,253}$)(^(?:[a-zA-Z0-9](?:(?:[a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$)" >/dev/null; then
return 0
else
return 1
fi
}
##Check Port
function check_port() {
local PORT=$1
VALID_CHECK=$(echo $PORT|awk '$1<=65535&&$1>=1{print "yes"}')
if echo $PORT |grep -E "^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]{1}|6553[0-5])$" >/dev/null; then
if [[ $VALID_CHECK == "yes" ]]; then
return 0
else
return 1
fi
else
return 1
fi
}
while :; do echo
echo "请确认你此台服务器是全新干净的,以防此脚本相关操作对正在运行的服务造成影响(不可逆)。"
read -p "请确认是否继续执行,输入 [y/n]: " ensure_yn
if [[ ! "${ensure_yn}" =~ ^[y,n]$ ]]; then
echo "输入有误,请输入 y 或 n"
else
break
fi
done
if [[ "${ensure_yn}" = n ]]; then
exit 0
fi
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用的本机IP: " PWD_SELF_SERVICE_IP
check_ip ${PWD_SELF_SERVICE_IP}
if [[ $? -ne 0 ]]; then
echo "---输入的IP地址格式有误请重新输入。"
else
break
fi
done
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用的端口(不要和Nginx一样): " PWD_SELF_SERVICE_PORT
check_port ${PWD_SELF_SERVICE_PORT}
if [[ $? -ne 0 ]]; then
echo "---输入的端口有误,请重新输入。"
else
break
fi
done
echo "======================================================================="
while :; do echo
read -p "请输入密码自助平台使用域名例如pwd.abc.com不需要加http://或https:// " PWD_SELF_SERVICE_DOMAIN
check_domain ${PWD_SELF_SERVICE_DOMAIN}
if [[ $? -ne 0 ]]; then
echo "---输入的域名格式有误,请重新输入。"
else
break
fi
done
##当前脚本的绝对路径
SHELL_FOLDER=$(dirname $(readlink -f "$0"))
echo "关闭SELINUX"
sudo setenforce 0
sudo sed -i 's@SELINUX=*@SELINUX=disabled@g' /etc/selinux/config
echo "DONE....."
echo "关闭防火墙"
sudo systemctl disable firewalld
sudo systemctl stop firewalld
echo "DONE....."
echo "初始化编译环境----------"
sudo yum install gcc patch libffi-devel python-devel zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel wget psmisc -y
echo "======================================================================="
echo "初始化编译环境完成"
echo "======================================================================="
##Quick install nginx
echo "======================================================================="
echo "安装 Nginx"
sudo cat << EOF > /etc/yum.repos.d/nginx.repo
[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/7/\$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true
EOF
sudo yum makecache fast
sudo yum install nginx -y
if [[ $? -eq 0 ]]
then
sudo systemctl enable nginx
sudo systemctl start nginx
echo "======================================================================="
echo "nginx 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "nginx 安装失败!"
echo "======================================================================="
exit 1
fi
##install python3
##如果之前用此脚本安装过python3后续就不会再次安装。
PYTHON_VER='3.8.9'
PYTHON_INSTALL_DIR=/usr/share/python-${PYTHON_VER}
if [[ -f "${PYTHON_INSTALL_DIR}/bin/python3" ]]
then
echo "己发现Python3将不会安装。"
else
if [[ -f "Python-${PYTHON_VER}.tar.xz" ]]
then
echo "将安装Python${PYTHON_VER}"
tar xf Python-${PYTHON_VER}.tar.xz
cd Python-${PYTHON_VER}
sudo ./configure --prefix=${PYTHON_INSTALL_DIR} && make && make install
else
echo "脚本目录下没有发现Python${PYTHON_VER}.tar.xz将会下载python ${PYTHON_VER}"
sudo wget https://www.python.org/ftp/python/${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz
if [[ $? -eq 0 ]]; then
tar xf Python-${PYTHON_VER}.tar.xz
cd Python-${PYTHON_VER}
sudo ./configure --prefix=${PYTHON_INSTALL_DIR} && make && make install
else
echo "下载${PYTHON_VER}/Python-${PYTHON_VER}.tar.xz失败请重新运行本脚本再次重试"
fi
fi
if [[ $? -eq 0 ]]
then
echo "创建python3和pip3的软件链接"
cd ${SHELL_FOLDER}
sudo ln -svf ${PYTHON_INSTALL_DIR}/bin/python3 /usr/bin/python3
sudo ln -svf ${PYTHON_INSTALL_DIR}/bin/pip3 /usr/bin/pip3
echo "======================================================================="
echo "Python3 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "Python3 安装失败!"
echo "======================================================================="
exit 1
fi
fi
##修改PIP源为国内
mkdir -p ~/.pip
cat << EOF > ~/.pip/pip.conf
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
[install]
trusted-host=pypi.tuna.tsinghua.edu.cn
EOF
cd ${SHELL_FOLDER}
echo "====升级pip================"
/usr/bin/pip3 install --upgrade pip
/usr/bin/pip3 install -r requestment
if [[ $? -eq 0 ]]
then
echo "======================================================================="
echo "Pip3 requestment 安装成功!"
echo "======================================================================="
else
echo "======================================================================="
echo "Pip3 requestment 安装失败!"
echo "======================================================================="
exit 1
fi
##处理配置文件
echo "======================================================================="
echo "处理uwsgi.ini配置文件"
CPU_NUM=$(cat /proc/cpuinfo | grep processor | wc -l)
sed -i "s@CPU_NUM@${CPU_NUM}@g" ${SHELL_FOLDER}/uwsgi.ini
sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgi.ini
sed -i "s@PWD_SELF_SERVICE_IP@${PWD_SELF_SERVICE_IP}@g" ${SHELL_FOLDER}/uwsgi.ini
sed -i "s@PWD_SELF_SERVICE_PORT@${PWD_SELF_SERVICE_PORT}@g" ${SHELL_FOLDER}/uwsgi.ini
echo "处理uwsgi.ini配置文件完成"
echo
echo "处理uwsgiserver启动脚本"
sed -i "s@PWD_SELF_SERVICE_HOME@${SHELL_FOLDER}@g" ${SHELL_FOLDER}/uwsgiserver
sed -i "s@PYTHON_INSTALL_DIR@${PYTHON_INSTALL_DIR}@g" ${SHELL_FOLDER}/uwsgiserver
alias cp='cp'
cp -f ${SHELL_FOLDER}/uwsgiserver /etc/init.d/uwsgiserver
chmod +x /etc/init.d/uwsgiserver
chkconfig uwsgiserver on
echo "处理uwsgiserver启动脚本完成"
echo
sed -i "s@PWD_SELF_SERVICE_DOMAIN@${PWD_SELF_SERVICE_DOMAIN}@g" ${SHELL_FOLDER}/conf/local_settings.py
##Nginx vhost配置
cat << EOF > /etc/nginx/conf.d/pwdselfservice.conf
server {
listen 80;
server_name ${PWD_SELF_SERVICE_DOMAIN} ${PWD_SELF_SERVICE_IP};
location / {
proxy_pass http://${PWD_SELF_SERVICE_IP}:${PWD_SELF_SERVICE_PORT};
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
access_log off;
}
EOF
rm -f /etc/nginx/conf.d/default.conf
systemctl restart nginx
echo
echo "======================================================================="
echo
echo "密码自助服务平台的访问地址是http://${PWD_SELF_SERVICE_DOMAIN}或http://${PWD_SELF_SERVICE_IP}"
echo "请确保以上域名能正常解析,否则使用域名无法访问。"
echo
echo "Uwsgi启动/etc/init.d/uwsgi start"
echo "Uwsgi停止/etc/init.d/uwsgi stop"
echo "Uwsgi重启/etc/init.d/uwsgi restart"
echo
echo
echo "文件${SHELL_FOLDER}/conf/local_setting.py中配置参数请自动确认下是否完整"
echo
echo "======================================================================="

70
conf/local_settings.py Normal file
View File

@ -0,0 +1,70 @@
# ##########################################################################
# 字符串前面的格式编码不要去掉了,主要是为了解决特殊字符被转义的问题。 #
# ##########################################################################
# ########## AD配置修改为自己的
# AD主机可以是IP或主机域名例如可以是: abc.com或172.16.122.1
LDAP_HOST = r'修改成自己的'
# AD域控的DOMAIN例如比如你的域名是abc.com那么这里的LDAP_DOMAIN就是abc
# NTLM认证必须是domain\username
LDAP_DOMAIN = r'修改成自己的'
# 用于登录AD做用户信息处理的账号需要有修改用户账号密码或信息的权限。
# AD账号例如pwdadmin
LDAP_LOGIN_USER = r'修改成自己的'
# 密码
LDAP_LOGIN_USER_PWD = r'修改为自己的'
# BASE DN账号的查找DN路径例如'DC=abc,DC=com'可以指定到OU之下例如'OU=RD,DC=abc,DC=com'。
# BASE_DN限制得越细搜索用户的目录也就越小一般情况下可以通过SEARCH_FILTER来过滤
BASE_DN = r'修改成自己的'
# ldap的search_filter如果需要修改请保持用户账号部分为 点位符{0} (代码中通过占位符引入账号)
# 例如AD的用户账号属性是sAMAccountName那么匹配的账号请配置成sAMAccountName={0}
# LDAP中用户账号属性可能是uuid那么匹配的账号请配置成uuid={0}
# 如果想限制用户在哪个组的才能使用,可以写成这样:
# r'(&(objectClass=user)(memberof=CN=mis,OU=Groups,OU=OnTheJob,DC=abc,DC=com)(sAMAccountName={0}))', memberof 是需要匹配的组
# 默认配置是AD环境的查询语句可以自行使用Apache Directory Studio测试后再配置
SEARCH_FILTER = r'(&(objectclass=user)(sAMAccountName={0}))'
# 是否启用SSL,
# 注意AD中必须使用SSL才能修改密码这里被坑了N久...,自行部署下AD的证书服务并颁发CA证书重启服务器生效。具体教程百度一下有很多。
# 如果使用Openldap这里根据实际情况调整
LDAP_USE_SSL = True
# 连接的端口如果启用SSL默认是636否则就是389
LDAP_CONN_PORT = 636
# 验证的类型
# 钉钉 / 企业微信,自行修改
# 值是DING / WEWORK
INTEGRATION_APP_TYPE = 'WEWORK'
# ########## 钉钉 《如果不使用钉钉,可不用配置》##########
# 钉钉企业ID <CorpId>,修改为自己的
DING_CORP_ID = '修改为自己的'
# 钉钉企业内部开发内部H5微应用或小程序用于读取企业内部用户信息
DING_AGENT_ID = r'修改为自己的'
DING_APP_KEY = r'修改为自己的'
DING_APP_SECRET = r'修改为自己的'
# 移动应用接入 主要为了实现通过扫码拿到用户的unionid
DING_MO_APP_ID = r'修改为自己的'
DING_MO_APP_SECRET = r'修改为自己的'
# ####### 企业微信《如果不使用企业微信,可不用配置》 ##########
# 企业微信的企业ID
WEWORK_CORP_ID = r'修改为自己的'
# 应用的AgentId
WEWORK_AGENT_ID = r'修改为自己的'
# 应用的Secret
WEWORK_AGNET_SECRET = r'修改为自己的'
# 主页域名钉钉跳转等需要指定域名格式pwd.abc.com。
# 如果是自定义安装,请修改成自己的域名
HOME_URL = 'PWD_SELF_SERVICE_DOMAIN'
# 平台显示的标题
TITLE = 'Self-Service'

339156
log/log.log Normal file

File diff suppressed because it is too large Load Diff

16
manage.py Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pwdselfservice.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)

View File

@ -0,0 +1,14 @@
import sys
import datetime
from utils.storage.memorystorage import MemoryStorage
from traceback import format_exc
try:
cache_storage = MemoryStorage()
cache_storage.set('MemoryStorage', str(datetime.datetime.now()))
redis_get = cache_storage.get('MemoryStorage')
except Exception as e:
print("MemoryStorage Exception: {}".format(format_exc()))
sys.exit(1)

163
pwdselfservice/settings.py Normal file
View File

@ -0,0 +1,163 @@
import os
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
DEBUG = True
else:
DEBUG = False
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'nxnm3#&2tat_c2i6%$y74a)t$(3irh^gpwaleoja1kdv30fmcm'
ALLOWED_HOSTS = ['*']
# 不安全的内部初始密码,用于检验新密码
UN_SEC_PASSWORD = ['1qaz@WSX', '1234@Abc']
# 创建日志的路径
LOG_PATH = os.path.join(BASE_DIR, 'log')
# 如果地址不存在则会自动创建log文件夹
if not os.path.isdir(LOG_PATH):
os.mkdir(LOG_PATH)
LOGGING = {
'version': 1,
# 此选项开启表示禁用部分日志不建议设置为True
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '%(asctime)s %(levelname)s %(message)s'
},
'simple': {
'format': '%(asctime)s %(levelname)s %(message)s'
},
},
'filters': {
'require_debug_true': {
# 过滤器只有当setting的DEBUG = True时生效
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
# 日志保存文件
'filename': '%s/all.log' % LOG_PATH,
# 日志格式,与上边的设置对应选择
'formatter': 'verbose'
}
},
'loggers': {
'django': {
# 日志记录器
'handlers': ['file'],
'level': 'DEBUG',
'propagate': True,
}
},
}
# SESSION
# 只有在settings.SESSION_SAVE_EVERY_REQUEST 为True时才有效
SESSION_SAVE_EVERY_REQUEST = True
# 过期时间分钟
SESSION_COOKIE_AGE = 300
# False 会话cookie可以在用户浏览器中保持有效期。True关闭浏览器则Cookie失效。
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
# session使用的存储方式
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'resetpwd',
]
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'pwdselfservice.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')]
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'pwdselfservice.wsgi.application'
# 514 66050是AD中账号被禁用的特定代码这个可以在微软官网查到。
# 可能不是太准确,如果使用者能确定还有其它状态码,可以自行在此处添加
AD_ACCOUNT_DISABLE_CODE = [514, 66050]
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'
# STATIC_ROOT = 'static'
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]

12
pwdselfservice/urls.py Normal file
View File

@ -0,0 +1,12 @@
from django.urls import path
from django.views.generic.base import RedirectView
import resetpwd.views
urlpatterns = {
path("favicon.ico", RedirectView.as_view(url='static/img/favicon.ico')),
path('', resetpwd.views.index, name='index'),
path('auth', resetpwd.views.auth, name='auth'),
path('resetPassword', resetpwd.views.reset_password, name='resetPassword'),
path('unlockAccount', resetpwd.views.unlock_account, name='unlockAccount'),
path('messages', resetpwd.views.messages, name='messages'),
}

16
pwdselfservice/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for pwdselfservice project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'pwdselfservice.settings')
application = get_wsgi_application()

179
readme.md Normal file
View File

@ -0,0 +1,179 @@
### 初学Django时碰到的一个需求因为公司中很多员工在修改密码之后有一些关联的客户端或网页中的旧密码没有更新导致密码在尝试多次之后账号被锁为了减少这种让人头疼的重置解锁密码的操蛋工作自己做了一个自助修改小平台。
### 代码结构不咋样,但是能用,有需要的可以直接拿去用。
#### 场景说明:
因为本公司AD是早期已经在用用户的个人信息不是十分全面例如:用户手机号。
钉钉是后来才开始使用,钉钉默认是使用手机号登录。
用户自行重置密码时如果通过手机号来进行钉钉与AD之间的验证就行不通了。
### 逻辑:
>已经与之前不同,现在改成内嵌小应用,不再支持直接通过网页开始,之前的扫码方式有点多此一举的味道。
## <u>_**所能接受的账号规则**_ </u>
无论是钉钉、微信均是通过提取用户邮箱的前缀部分来作为关联AD的账号所以目前的识别逻辑就需要保证邮箱的前缀和AD的登录账号是一致的。
如果您的场景不是这样,请按自己的需求修改源代码适配。
### 提示:
```
AD必须使用SSL才能修改密码这里被坑了N久...
自行部署下AD的证书服务并颁发CA证书重启服务器生效。
具体教程百度一下,有很多。
```
### 本次升级、修复,请使用最新版:
+ 升级Python版本为3.8
+ 升级Django到3.2
+ 修复用户名中使用\被转义的问题
+ 重写了dingding模块因为dingding开发者平台接口鉴权的一些变动之前的一些接口不能再使用本次重写。
+ 重写了ad模块修改账号的一些判断逻辑。
+ 重写了用户账号的格式兼容现在用户账号可以兼容username、DOMAIN\username、username@abc.com这三种格式。
+ 优化了整体的代码逻辑,去掉一些冗余重复的代码。
### 2022/12/16 -- 更新:
+ 修改钉钉、企业微信直接通过企业内部免密登录授权或验证的方式实现用户信息的获取直接通过软件内部工作平台打开废弃扫码方式由于API接口的权限问题一些关键数据已经不再支持通过扫码获取
### 2023/01/15 -- 更新:
+ 兼容PC与移动端的显示使用Layui
+ 修复一些BUG
+ 移除auto-install.sh中的redis部署步骤
+ 抽出应用中的授权验证跳转的代码单独做成一个auth页面可实现选择首页是进入修改密码还是自动跳转重置页面。
+ 调整部分文件说明
+ 添加日志装饰器记录请求日志
+ 优化ad_ops中异常的处理
**如果想在点击应用之后自动跳转到重置页面请将回调接口配置成YOURDOMAIN.com/auth**
## 线上环境需要的基础环境:
+ Python 3.8.9 (可自行下载源码包放到项目目录下,使用一键安装)
+ Nginx
+ Uwsgi
### 界面效果
<img alt="截图10" width="500" src="screenshot/QQ截图20230116152954.png">
<img alt="截图10" width="500" src="screenshot/212473880-4a59c535-85bb-42d2-a99a-899265c83136.png">
<img alt="截图10" width="500" src="screenshot/212474222-e1c13e1b-bb6f-4523-b040-24a65055d681.png">
### 移动端
<img alt="截图10" width="500" src="screenshot/212474177-dd68b0c9-81cc-4eb0-9196-e760784e3f69.jpg">
<img alt="截图10" width="500" src="screenshot/212474293-0cd60898-22c3-4258-ac4c-dfee52a6cf1e.png">
## 钉钉必要条件:
#### 创建企业内部应用
* 在钉钉工作台中通过“自建应用”创建应用选择“企业内部开发”创建H5微应用在应用首页中获取应用的AgentId、AppKey、AppSecret。
* 应用需要权限:通讯录只读权限、邮箱等个人信息,范围是全部员工或自行选择
* 应用安全域名和IP一定要配置否则无法返回接口数据。
> **如果想实现进入应用就自动授权跳转重置页面可将回调域名指定向pwd.abc.com/auth**
参考截图配置:
![截图3](screenshot/h5微应用.png)
![截图4](screenshot/创建H5微应用03.png)
![截图5](screenshot/创建H5微应用04.png)
![截图5](screenshot/创建H5微应用--版本管理与发布.png)
#### 移动接入应用--登录权限:
> 废弃,已经不再需要,如果之前有配置,可以删除!!
## 企业微信必要条件:
* 创建应用记录下企业的CorpId应用的ID和Secret。
> **如果想实现进入应用就自动授权跳转重置页面可将回调域名指定向pwd.abc.com/auth**
参考截图:
![截图7](screenshot/微扫码13.png)
![截图8](screenshot/微信小应用01-应用主机.png)
![截图9](screenshot/微信小应用01-应用主页-配置.png)
![截图10](screenshot/微信小应用01-网页授权及J-SDK配置.png)
![截图10](screenshot/微信小应用01-企业可信IP.png)
## 飞书必要条件:
* 暂时没时间,做不了,已经剔除了!
## 如果你觉得这个小工具对你有帮忙的话,可以请我喝杯咖啡~😁😁😁
<img alt="截图10" height="400" src="screenshot/143fce31873f4d7a4ecd7a7b8c6a24c.png" width="300"/>
<img alt="截图10" height="400" src="screenshot/微信图片_20221220140900.png" width="300"/>
## 使用脚本自动部署:
使用脚本自动快速部署只适合Centos其它发行版本的Linux请自行修改相关命令。
### 把整个项目目录上传到新的服务器上
#### 先修改配置文件,按自己实际的配置修改项目配置文件:
修改conf/local_settings.py中的参数按自己的实际参数修改
### 执行部署脚本
```shell
chmod +x auto-install.sh
./auto-install.sh
```
等待所有安装完成。
#### 以上配置修改完成之后,则可以通过配置的域名直接访问。
# 手动部署:
#### 自行安装完python3之后使用python3目录下的pip3进行安装依赖
#### 我自行安装的Python路径为/usr/local/python3
项目目录下的requestment文件里记录了所依赖的相关python模块安装方法
>/usr/local/python3/bin/pip3 install -r requestment
等待所有模块安装完成之后进行下一步。
### 按自己实际的配置修改项目配置参数:
修改conf/local_settings.py中的参数按自己的实际参数修改
```
安装完依赖后,直接执行
/usr/local/python3/bin/python3 manager.py runserver x.x.x.x:8000
即可临时访问项目线上不适用这种方法线上环境请使用uwsgi。
## 修改uwsig.ini配置:
IP和路径按自己实际路径修改
```
## 通过uwsgiserver启动
```shell
请自行修改将脚本修改完之后
复制到/etc/init.d/,给予执行权限。
执行/etc/init.d/uwsigserver start 启动
```
## 自行部署Nginx然后添加Nginx配置
#### Nginx配置
Nginx Server配置
* proxy_pass的IP地址改成自己的服务器IP
* 配置可自己写一个vhost或直接加在nginx.conf中
``` nginx
server {
listen 80;
server_name pwd.abc.com;
location / {
proxy_pass http://192.168.x.x:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
access_log off;
}
```
- 执行Nginx reload操作重新加载配置

11
requestment Normal file
View File

@ -0,0 +1,11 @@
Django==3.2
pycryptodome==3.10.1
attrs==21.2.0
python-dateutil==2.8.1
dingtalk-sdk==1.3.8
cryptography==3.4.7
ldap3==2.9
django-redis==4.12.1
django-redis==4.12.1
requests==2.28.1
uwsgi==2.0.21

0
resetpwd/__init__.py Normal file
View File

5
resetpwd/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ResetpwdConfig(AppConfig):
name = 'resetpwd'

33
resetpwd/form.py Normal file
View File

@ -0,0 +1,33 @@
from django.forms import fields as c_fields
from django import forms as c_forms
class CheckForm(c_forms.Form):
new_password = c_fields.RegexField(
'(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}',
# 密码必须同时包含大写、小写、数字和特殊字符其中三项且至少8位
strip=True,
min_length=8,
max_length=30,
error_messages={'required': '新密码不能为空.',
'invalid': '密码必须包含数字,字母、特殊字符',
'min_length': "密码长度不能小于8个字符",
'max_length': "密码长度不能大于30个字符"}
)
old_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
ensure_password = c_fields.CharField(error_messages={'required': '确认密码不能为空'})
username = c_fields.CharField(error_messages={'required': '账号不能为空', 'invalid': '账号格式错误'})
def clean(self):
pwd0 = self.cleaned_data.get('old_password')
pwd1 = self.cleaned_data.get('new_password')
pwd2 = self.cleaned_data.get('ensure_password')
if pwd1 == pwd2:
pass
elif pwd0 == pwd1:
# 这里异常模块导入要放在函数里面,放到文件开头有时会报错,找不到
from django.core.exceptions import ValidationError
raise ValidationError('新旧密码不能一样')
else:
from django.core.exceptions import ValidationError
raise ValidationError('新密码和确认密码输入不一致')

128
resetpwd/utils.py Normal file
View File

@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @FileName utils.py
# @Software:
# @Author: Leven Xiang
# @Mail: xiangle0109@outlook.com
# @Date 2021/5/20 8:47
from django.shortcuts import render
import logging
from ldap3.core.exceptions import LDAPException
from django.conf import settings
import os
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
from conf.local_settings_dev import *
else:
from conf.local_settings import *
logger = logging.getLogger('django')
def code_2_user_detail(ops, home_url, code):
"""
临时授权码换取userinfo
"""
_, s, e = ops.get_user_detail(code=code, home_url=home_url)
return _, s, e
def code_2_user_info_with_oauth2(ops, request, msg_template, home_url, code):
"""
临时授权码换取userinfo
"""
_status, user_id = ops.get_user_id_by_code(code)
# 判断 user_id 在本企业钉钉/微信中是否存在
if not _status:
context = {'global_title': TITLE,
'msg': '获取userid失败错误信息{}'.format(user_id),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return False, context, user_id
detail_status, user_info = ops.get_user_detail_by_user_id(user_id)
if not detail_status:
context = {'global_title': TITLE,
'msg': '获取用户信息失败,错误信息:{}'.format(user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return False, context, user_info
return True, user_id, user_info
def ops_account(ad_ops, request, msg_template, home_url, username, new_password):
"""
ad 账号操作判断账号状态重置密码或解锁账号
"""
try:
print("ops_account: {}".format(username))
_status, _account = ad_ops.ad_ensure_user_by_account(username=username)
if not _status:
context = {'global_title': TITLE,
'msg': "账号[%s]在AD中不存在请确认当前钉钉扫码账号绑定的邮箱是否和您正在使用的邮箱一致或者该账号己被禁用\n猜测:您的账号或邮箱是否是带有数字或其它字母区分?" % username,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
_status, account_code = ad_ops.ad_get_user_status_by_account(username)
if _status and account_code in settings.AD_ACCOUNT_DISABLE_CODE:
context = {'global_title': TITLE,
'msg': "此账号状态为己禁用请联系HR确认账号是否正确。",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
elif not _status:
context = {'global_title': TITLE,
'msg': "错误:{}".format(account_code),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
if new_password:
reset_status, result = ad_ops.ad_reset_user_pwd_by_account(username=username, new_password=new_password)
if reset_status:
# 重置密码并执行一次解锁,防止重置后账号还是锁定状态。
unlock_status, result = ad_ops.ad_unlock_user_by_account(username)
if unlock_status:
context = {'global_title': TITLE,
'msg': "密码己修改成功,请妥善保管。你可以点击修改密码或直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {'global_title': TITLE,
'msg': "密码未修改/重置成功,错误信息:{}".format(result),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
else:
unlock_status, result = ad_ops.ad_unlock_user_by_account(username)
if unlock_status:
context = {'global_title': TITLE,
'msg': "账号己解锁成功。你可以点击返回主页或直接关闭此页面!",
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
else:
context = {'global_title': TITLE,
'msg': "账号未能解锁,错误信息:{}".format(result),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
except LDAPException as l_e:
context = {'global_title': TITLE,
'msg': "账号未能解锁,错误信息:{}".format(l_e),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)

307
resetpwd/views.py Normal file
View File

@ -0,0 +1,307 @@
import json
import logging
import os
from django.shortcuts import render
from utils.ad_ops import AdOps
import urllib.parse as url_encode
from utils.format_username import format2username, get_user_is_active, get_email_from_userinfo
from .form import CheckForm
from .utils import code_2_user_detail, ops_account
from django.conf import settings
from utils.logger_filter import decorator_request_logger
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
from conf.local_settings_dev import INTEGRATION_APP_TYPE, DING_MO_APP_ID, WEWORK_CORP_ID, WEWORK_AGENT_ID, HOME_URL, \
DING_CORP_ID, TITLE
else:
from conf.local_settings import INTEGRATION_APP_TYPE, DING_MO_APP_ID, WEWORK_CORP_ID, WEWORK_AGENT_ID, HOME_URL, \
DING_CORP_ID, TITLE
msg_template = 'messages.html'
logger = logging.getLogger('django')
class PARAMS(object):
if INTEGRATION_APP_TYPE == 'DING':
corp_id = DING_CORP_ID
app_id = DING_MO_APP_ID
agent_id = None
AUTH_APP = '钉钉'
from utils.dingding_ops import DingDingOps
ops = DingDingOps()
elif INTEGRATION_APP_TYPE == 'WEWORK':
corp_id = None
app_id = WEWORK_CORP_ID
agent_id = WEWORK_AGENT_ID
AUTH_APP = '微信'
from utils.wework_ops import WeWorkOps
ops = WeWorkOps()
scan_params = PARAMS()
_ops = scan_params.ops
@decorator_request_logger(logger)
def auth(request):
home_url = '%s://%s' % (request.scheme, HOME_URL)
corp_id = scan_params.corp_id
app_id = scan_params.app_id
agent_id = scan_params.agent_id
scan_app = scan_params.AUTH_APP
unsecpwd = settings.UN_SEC_PASSWORD
redirect_url = url_encode.quote(home_url + '/resetPassword')
app_type = INTEGRATION_APP_TYPE
global_title = TITLE
if request.method == 'GET':
code = request.GET.get('code', None)
username = request.GET.get('username', None)
# 如果满足,说明已经授权免密登录
if username and code and request.session.get(username) == code:
context = {'global_title': TITLE,
'username': username,
'code': code,
}
return render(request, 'reset_password.html', context)
return render(request, 'auth.html', locals())
else:
logger.error('[异常] 请求方法:%s,请求路径%s' % (request.method, request.path))
@decorator_request_logger(logger)
def index(request):
"""
用户自行修改密码/首页
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
scan_app = scan_params.AUTH_APP
global_title = TITLE
if request.method == 'GET':
return render(request, 'index.html', locals())
elif request.method == 'POST':
# 对前端提交的数据进行二次验证,防止恶意提交简单密码或篡改账号。
check_form = CheckForm(request.POST)
if check_form.is_valid():
form_obj = check_form.cleaned_data
username = form_obj.get("username")
old_password = form_obj.get("old_password")
new_password = form_obj.get("new_password")
else:
_msg = check_form
logger.error('[异常] 请求方法:%s,请求路径:%s,错误信息:%s' % (request.method, request.path, _msg))
context = {'global_title': TITLE,
'msg': _msg,
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
# 格式化用户名
_, username = format2username(username)
if _ is False:
context = {'global_title': TITLE,
'msg': username,
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
# 检测账号状态
auth_status, auth_result = AdOps().ad_auth_user(username=username, password=old_password)
if not auth_status:
context = {'global_title': TITLE,
'msg': str(auth_result),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
return ops_account(AdOps(), request, msg_template, home_url, username, new_password)
else:
context = {'global_title': TITLE,
'msg': "不被接受的认证信息,请重新尝试认证授权。",
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
@decorator_request_logger(logger)
def reset_password(request):
"""
钉钉扫码并验证信息通过之后在重置密码页面将用户账号进行绑定
:param request:
:return:
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
if request.method == 'GET':
code = request.GET.get('code', None)
username = request.GET.get('username', None)
# 如果满足,说明已经授权免密登录
if username and code and request.session.get(username) == code:
context = {'global_title': TITLE,
'username': username,
'code': code,
}
return render(request, 'reset_password.html', context)
else:
if code:
logger.info('[成功] 请求方法:%s,请求路径:%sCode%s' % (request.method, request.path, code))
else:
logger.error('[异常] 请求方法:%s,请求路径:%s未能拿到Code。' % (request.method, request.path))
context = {'global_title': TITLE,
'msg': "错误,临时授权码己失效,请尝试重新认证授权..",
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
try:
# 用code换取用户基本信息
_status, user_id, user_info = code_2_user_detail(_ops, home_url, code)
if not _status:
return render(request, msg_template, user_id)
# 账号在企业微信或钉钉中是否是激活的
_, res = get_user_is_active(user_info)
if not _:
context = {'global_title': TITLE,
'msg': '当前扫码的用户未激活或可能己离职,用户信息如下:%s' % user_info,
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return render(request, msg_template, context)
except Exception as callback_e:
context = {'global_title': TITLE,
'msg': "错误[%s],请与管理员联系." % str(callback_e),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
logger.error('[异常] %s' % str(callback_e))
return render(request, msg_template, context)
# 通过user_info拿到用户邮箱并格式化为username
_, email = get_email_from_userinfo(user_info)
if not _:
context = {'global_title': TITLE,
'msg': email,
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
_, username = format2username(email)
if _ is False:
context = {'global_title': TITLE,
'msg': username,
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
# 如果邮箱能提取到,则格式化之后,提取出账号提交到前端绑定
if username:
request.session[username] = code
context = {'global_title': TITLE,
'username': username,
'code': code,
}
return render(request, 'reset_password.html', context)
else:
context = {'global_title': TITLE,
'msg': "{},您好,企业{}中未能找到您账号的邮箱配置请联系HR完善信息。".format(
user_info.get('name'), scan_params.AUTH_APP),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
# 重置密码页面,输入新密码后点击提交
elif request.method == 'POST':
username = request.POST.get('username')
code = request.POST.get('code')
if code and request.session.get(username) == code:
_new_password = request.POST.get('new_password').strip()
try:
return ops_account(ad_ops=AdOps(), request=request, msg_template=msg_template, home_url=home_url,
username=username, new_password=_new_password)
except Exception as reset_e:
context = {'global_title': TITLE,
'msg': "错误[%s],请与管理员联系." % str(reset_e),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
logger.error('[异常] %s' % str(reset_e))
return render(request, msg_template, context)
finally:
del request.session[username]
else:
context = {'global_title': TITLE,
'msg': "认证已经失效,可尝试从重新认证授权。",
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
@decorator_request_logger(logger)
def unlock_account(request):
"""
解锁账号
:param request:
:return:
"""
home_url = '%s://%s' % (request.scheme, HOME_URL)
if request.method == 'GET':
code = request.GET.get('code')
username = request.GET.get('username')
if code and request.session.get(username) == code:
context = {'global_title': TITLE,
'username': username,
'code': code,
}
return render(request, 'unlock.html', context)
else:
context = {'global_title': TITLE,
'msg': "{},您好,当前会话可能已经过期,请再试一次吧。".format(username),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
if request.method == 'POST':
username = request.POST.get('username')
code = request.POST.get('code')
if request.session.get(username) and request.session.get(username) == code:
try:
return ops_account(AdOps(), request, msg_template, home_url, username, None)
except Exception as reset_e:
context = {'global_title': TITLE,
'msg': "错误[%s],请与管理员联系." % str(reset_e),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
logger.error('[异常] %s' % str(reset_e))
return render(request, msg_template, context)
finally:
del request.session[username]
else:
context = {'global_title': TITLE,
'msg': "认证已经失效,请尝试从重新进行认证授权。",
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return render(request, msg_template, context)
@decorator_request_logger(logger)
def messages(request):
_msg = request.GET.get('msg')
button_click = request.GET.get('button_click')
button_display = request.GET.get('button_display')
context = {'global_title': TITLE,
'msg': _msg,
'button_click': button_click,
'button_display': button_display
}
return render(request, msg_template, context)

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

BIN
screenshot/h5微应用.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 KiB

BIN
screenshot/微扫码13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
screenshot/微扫码14.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
screenshot/微扫码15.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
screenshot/微扫码16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

BIN
screenshot/扫码成功.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

54
static/css/style.css Normal file
View File

@ -0,0 +1,54 @@
html, body {
width: 100%;
height: 100%;
}
body {
font-family: "Microsoft Yahei", serif;
background: #fafafa;
}
a:link {
text-decoration: none;
}
a:hover {
cursor:pointer;
}
.a-middle-text {
text-align: center;
}
.grid {
padding: 1px;
line-height: 50px;
margin-bottom: 15px;
}
.middle-header {
margin-top: 30px;
line-height: 50px;
margin-bottom: 5%;
border-color: #b7b7b7;
font-weight: bold;
text-align: center;
}
.middle-header-title {
margin-top: 30px;
line-height: 50px;
border-color: #b7b7b7;
font-weight: bold;
}
.layui-panel {
padding: 10px 20px;
}
.layui-elem-fieldset {
margin-top: 10%;
border-color: #b7b7b7;
border-radius: 6px;
background: #ffffff;
}

BIN
static/img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
static/img/icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/img/mima-icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/img/unlock.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
static/img/user-icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

1
static/js/html5.min.js vendored Normal file
View File

@ -0,0 +1 @@
(function(e,t){function n(e,t){var n=e.createElement("p"),i=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",i.insertBefore(n.lastChild,i.firstChild)}function i(){var e=m.elements;return"string"==typeof e?e.split(" "):e}function r(e){var t={},n=e.createElement,r=e.createDocumentFragment,o=r();e.createElement=function(e){m.shivMethods||n(e);var i;return i=t[e]?t[e].cloneNode():g.test(e)?(t[e]=n(e)).cloneNode():n(e),i.canHaveChildren&&!f.test(e)?o.appendChild(i):i},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(e){return t[e]=n(e),o.createElement(e),'c("'+e+'")'})+");return n}")(m,o)}function o(e){var t;return e.documentShived?e:(m.shivCSS&&!d&&(t=!!n(e,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),h||(t=!r(e)),t&&(e.documentShived=t),e)}function a(e){for(var t,n=e.getElementsByTagName("*"),r=n.length,o=RegExp("^(?:"+i().join("|")+")$","i"),a=[];r--;)t=n[r],o.test(t.nodeName)&&a.push(t.applyElement(s(t)));return a}function s(e){for(var t,n=e.attributes,i=n.length,r=e.ownerDocument.createElement(b+":"+e.nodeName);i--;)t=n[i],t.specified&&r.setAttribute(t.nodeName,t.nodeValue);return r.style.cssText=e.style.cssText,r}function l(e){for(var t,n=e.split("{"),r=n.length,o=RegExp("(^|[\\s,>+~])("+i().join("|")+")(?=[[\\s,>+~#.:]|$)","gi"),a="$1"+b+"\\:$2";r--;)t=n[r]=n[r].split("}"),t[t.length-1]=t[t.length-1].replace(o,a),n[r]=t.join("}");return n.join("{")}function c(e){for(var t=e.length;t--;)e[t].removeNode()}function u(e){var t,i,r=e.namespaces,o=e.parentWindow;return!y||e.printShived?e:(r[b]===void 0&&r.add(b),o.attachEvent("onbeforeprint",function(){for(var r,o,s,c=e.styleSheets,u=[],d=c.length,h=Array(d);d--;)h[d]=c[d];for(;s=h.pop();)if(!s.disabled&&v.test(s.media)){for(r=s.imports,d=0,o=r.length;o>d;d++)h.push(r[d]);try{u.push(s.cssText)}catch(p){}}u=l(u.reverse().join("")),i=a(e),t=n(e,u)}),o.attachEvent("onafterprint",function(){c(i),t.removeNode(!0)}),e.printShived=!0,e)}var d,h,p=e.html5||{},f=/^<|^(?:button|form|map|select|textarea|object|iframe)$/i,g=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i;(function(){var n=t.createElement("a");n.innerHTML="<xyz></xyz>",d="hidden"in n,d&&"function"==typeof injectElementWithStyles&&injectElementWithStyles("#modernizr{}",function(t){t.hidden=!0,d="none"==(e.getComputedStyle?getComputedStyle(t,null):t.currentStyle).display}),h=1==n.childNodes.length||function(){try{t.createElement("a")}catch(e){return!0}var n=t.createDocumentFragment();return n.cloneNode===void 0||n.createDocumentFragment===void 0||n.createElement===void 0}()})();var m={elements:p.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:p.shivCSS!==!1,shivMethods:p.shivMethods!==!1,type:"default",shivDocument:o};e.html5=m,o(t);var v=/^$|\b(?:all|print)\b/,b="html5shiv",y=!h&&function(){var n=t.documentElement;return t.namespaces!==void 0&&t.parentWindow!==void 0&&n.applyElement!==void 0&&n.removeNode!==void 0&&e.attachEvent!==void 0}();m.type+=" print",m.shivPrint=u,u(t)})(this,document);

5
static/js/respond.min.js vendored Normal file
View File

@ -0,0 +1,5 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
* */
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='&shy;<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-view{display:block;position:relative;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#fafafa;color:#333;font-family:Courier New;font-size:13px}.layui-code-title{position:relative;padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee;font-size:12px}.layui-code-title>.layui-code-about{position:absolute;right:10px;top:0;color:#b7b7b7}.layui-code-about>a{padding-left:10px}.layui-code-view>.layui-code-ol,.layui-code-view>.layui-code-ul{position:relative;overflow:auto}.layui-code-view>.layui-code-ol>li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view>.layui-code-ol>li:first-child,.layui-code-view>.layui-code-ul>li:first-child{padding-top:10px}.layui-code-view>.layui-code-ol>li:last-child,.layui-code-view>.layui-code-ul>li:last-child{padding-bottom:10px}.layui-code-view>.layui-code-ul>li{position:relative;line-height:20px;padding:0 10px;list-style-type:none;*list-style-type:none;background-color:#fff}.layui-code-view pre{margin:0}.layui-code-dark{border:1px solid #0c0c0c;border-left-color:#3f3f3f;background-color:#0c0c0c;color:#c2be9e}.layui-code-dark>.layui-code-title{border-bottom:none}.layui-code-dark>.layui-code-ol>li,.layui-code-dark>.layui-code-ul>li{background-color:#3f3f3f;border-left:none}.layui-code-dark>.layui-code-ul>li{margin-left:6px}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 299 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
static/layui/layui.js Normal file

File diff suppressed because one or more lines are too long

41
templates/auth.html Normal file
View File

@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% load static %}
{% block headerjs %}<script type="text/javascript" src="{% static 'js/dingtalk.open.js' %}"></script>{% endblock %}
{% block paneltitle %}请稍后,授权信息认证中{% endblock %}
{% block middleblock %}
{% endblock %}
{% block footerjs %}
<script src="{% static 'layui/layui.js' %}"></script>
<script>
layui.use(['form', 'jquery', 'layer'], function () {
let layer = layui.layer,
$ = layui.jquery;
let re_url= ""
let index_load = layer.load(1, {shade: 0.4});
{% if app_type == 'DING' %}
dd.ready(() => {
dd.runtime.permission.requestAuthCode({corpId: '{{ corp_id }}'}).then((result) => {
re_url = '/resetPassword?code=' + result.code
window.parent.parent.location.href=re_url;
}).catch(err => {
layer.close(index_load)
layer.open({
title : '出错啦!'
,content: err
,btn: '关闭'
,btnAlign: 'c'
,yes: function(){
layer.closeAll();
}
});
});
});
{% elif app_type == 'WEWORK' %}
$(function () {
re_url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={{ app_id }}&agentid={{ agent_id }}&redirect_uri={{ redirect_url }}&response_type=code&scope=snsapi_privateinfo&state=#wechat_redirect"
window.parent.parent.location.href=re_url;
})
{% endif %}
});
</script>
{% endblock %}

57
templates/base.html Normal file
View File

@ -0,0 +1,57 @@
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta http-equiv="Pragma" content= "no-cache" />
<meta http-equiv="Cache-Control" content= "no-cache" />
<meta http-equiv="Expires" content= "0" />
<title>{{ global_title }}</title>
<link rel="stylesheet" href="{% static 'layui/css/layui.css' %}">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block headercss %}{% endblock %}
{% block headerjs %}{% endblock %}
</head>
<body>
<!--[if lt IE 9]>
<script src="{% static 'js/html5.min.js' %}"></script>
<script src="{% static 'js/respond.min.js' %}"></script>
<![endif]-->
<div class="layui-container">
<div class="layui-row">
<div class="layui-hide-xs layui-col-md1">
<div class="grid"></div>
</div>
<div class="layui-col-xs12 layui-col-md10">
<div class="grid">
<div class="middle-header">
<h1>{{ global_title }}</h1>
</div>
<div class="layui-panel">
<div class="layui-row">
{% block upmiddleheader %}{% endblock %}
</div>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 20px;">
<legend>
{% block paneltitle %}{% endblock %}
</legend>
</fieldset>
{% block middleblock %}{% endblock %}
</div>
</div>
<div class="grid">
{% block middleblockfoot %}
{% endblock %}
</div>
</div>
<div class="layui-hide-xs layui-col-md1">
<div class="grid"></div>
</div>
</div>
</div>
<script src="{% static 'layui/layui.js' %}" charset="utf-8"></script>
{% block footercss %}{% endblock %}
{% block footerjs %}{% endblock %}
</body>
</html>

BIN
templates/farvirate.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

72
templates/index.html Normal file
View File

@ -0,0 +1,72 @@
{% extends 'base.html' %}
{% load static %}
{% block headerjs %}<script type="text/javascript" src="{% static 'js/dingtalk.open.js' %}"></script>{% endblock %}
{% block paneltitle %}修改密码{% endblock %}
{% block middleblock %}
<div class="layui-row">
<form class="layui-form layui-form-pane" action="/" method="post" autocomplete="off">{% csrf_token %}
<div class="layui-form-item">
<label class="layui-form-label">账号</label>
<div class="layui-input-block">
<input type="text" name="username" lay-verify="required" lay-verType="tips" autocomplete="off" placeholder="请输入账号" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">旧密码</label>
<div class="layui-input-block">
<input type="password" lay-verify="required|newpass" lay-verType="tips" name="old_password" id="old_password" placeholder="请输入旧密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<div class="layui-input-block">
<input type="password" lay-verify="pass" lay-verType="tips" name="new_password" id="new_password" placeholder="请输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" lay-verify="pass|repass" lay-verType="tips" name="ensure_password" id="ensure_password" placeholder="再次确认新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<button type="submit" lay-submit="" class="layui-btn layui-btn-normal layui-btn-fluid">立即提交</button>
</div>
<div class="layui-form-item a-middle-text">
<span class="layui-breadcrumb">
<a class="layui-text" id="redirect_url" href="/auth"><i class="layui-icon layui-icon-refresh-3"></i> 重置/解锁账号</a>
</span>
</div>
</form>
</div>
{% endblock %}
{% block middleblockfoot %}
<blockquote class="layui-elem-quote layui-quote-nm">
新密码8至30位长度要求包含大小写字母及数字。<br><br>
如果密码己遗忘,可点击上方<b>[<i class="layui-icon layui-icon-refresh-3"></i> 重置/解锁账号]</b>使用⌊{{ scan_app }}⌉应用内免登录授权并通过身份验证后进行重置/解锁账号。<br>
* 如果有当弹出提示<b>是否同意授权</b>时,请务必<b>全部同意</b>,否则无法获取关键信息,导致无法正常使用重置/解锁账号!
</blockquote>
{% endblock %}
{% block footercss %}{% endblock %}
{% block footerjs %}
<script src="{% static 'layui/layui.js' %}"></script>
<script>
layui.use(['form', 'jquery', 'layer'], function () {
let form = layui.form,
$ = layui.jquery;
form.verify({
pass: [
/^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}$/,
'密码必须8到30位要求包含大小写字母、数字与字符且不能出现空格'
],
repass: function (value,item) {
if ($('#ensure_password').val() !== $('#new_password').val()) {
return '两次输入密码不一致!';
}},
newpass: function (value,item) {
if ($('#old_password').val() === $('#password').val()) {
return '新旧密码不能重复使用,请修正!';
}}});
});
</script>
{% endblock %}

15
templates/messages.html Normal file
View File

@ -0,0 +1,15 @@
{% extends 'base.html' %}
{% load static %}
{% block paneltitle %}结果{% endblock %}
{% block middleblock %}
<div class="layui-row">
<form class="layui-form layui-form-pane" method="get" autocomplete="off">{% csrf_token %}
<div class="layui-form-item">
{{ msg }}
</div>
<div class="layui-form-item">
<button type="button" class="layui-btn layui-btn-normal layui-btn-fluid" onclick="{{ button_click }}">{{ button_display }}</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends 'base.html' %}
{% load static %}
{% block paneltitle %}重置密码{% endblock %}
{% block middleblock %}
<div class="layui-row">
<form class="layui-form layui-form-pane" name="resetPassword" method="post" action="" autocomplete="off">{% csrf_token %}
<div class="layui-form-item">
<label class="layui-form-label">账号</label>
<div class="layui-input-block">
<input type="text" name="username" lay-verify="required" lay-verType="tips" autocomplete="off" readonly value="{{ username }}" class="layui-input">
<input type="hidden" id="code" name="code" readonly value="{{ code }}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">新密码</label>
<div class="layui-input-block">
<input type="password" lay-verify="pass" lay-verType="tips" name="new_password" id="new_password" placeholder="请输入新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label">确认密码</label>
<div class="layui-input-block">
<input type="password" lay-verify="pass|repass" lay-verType="tips" name="ensure_password" id="ensure_password" placeholder="再次确认新密码" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item">
<button type="submit" lay-submit="" class="layui-btn layui-btn-normal layui-btn-fluid">立即提交</button>
</div>
<div class="layui-form-item a-middle-text">
<span class="layui-breadcrumb">
<a class="layui-text" href="/"><i class="layui-icon layui-icon-prev"></i> 修改密码</a>
<a class="layui-text" id="redirect_url" href="/unlockAccount?code={{ code }}&username={{ username }}"><i class="layui-icon layui-icon-password"></i> 解锁账号</a>
</span>
</div>
</form>
</div>
{% endblock %}
{% block middleblockfoot %}
<blockquote class="layui-elem-quote layui-quote-nm">
新密码8至30位长度要求包含大小写字母及数字。
<p>会话有效期5分钟重置密码会自动解锁账号(己禁用的账号不会生效)</p>
</blockquote>
{% endblock %}
{% block footerjs %}
<script src="{% static 'layui/layui.js' %}"></script>
<script>
layui.use(['form', 'jquery',], function () {
let form = layui.form,
$ = layui.jquery;
form.verify({
pass: [
/^(?=.*[0-9])(?=.*[a-zA-Z])(?=.*[^a-zA-Z0-9]).{8,30}$/,
'密码必须8到30位要求包含大小写字母、数字与字符且不能出现空格'
],
repass: function (value,item) {
if ($('#ensure_password').val() !== $('#new_password').val()) {
return '两次输入密码不一致!';
}
}
});
});
</script>
{% endblock %}

31
templates/unlock.html Normal file
View File

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% load static %}
{% block paneltitle %}解锁账号{% endblock %}
{% block middleblock %}
<div class="layui-row">
<form class="layui-form layui-form-pane" name="unlockAccount" method="post" action="unlockAccount" autocomplete="off">{% csrf_token %}
<div class="layui-form-item">
<label class="layui-form-label">账号</label>
<div class="layui-input-block">
<input type="text" name="username" lay-verify="required" lay-verType="tips" autocomplete="off" readonly value="{{ username }}" class="layui-input">
<input type="hidden" id="code" name="code" readonly value="{{ code }}">
</div>
</div>
<div class="layui-form-item">
<button type="submit" lay-submit="" class="layui-btn layui-btn-normal layui-btn-fluid">立即提交</button>
</div>
<div class="layui-form-item a-middle-text">
<span class="layui-breadcrumb">
<a class="layui-text" href="/"><i class="layui-icon layui-icon-prev"></i> 修改密码</a>
<a class="layui-text" id="redirect_url" href="/resetPassword?code={{ code }}&username={{ username }}"><i class="layui-icon layui-icon-refresh-1"></i> 重置密码</a>
</span>
</div>
</form>
</div>
{% endblock %}
{% block middleblockfoot %}
<blockquote class="layui-elem-quote layui-quote-nm">
新密码8至30位长度要求包含大小写字母及数字。
<p>会话有效期5分钟</p>
</blockquote>
{% endblock %}

263
utils/ad_ops.py Normal file
View File

@ -0,0 +1,263 @@
from ldap3 import *
from ldap3.core.exceptions import LDAPInvalidCredentialsResult, LDAPOperationResult, LDAPExceptionError, LDAPException, \
LDAPSocketOpenError
from ldap3.core.results import *
from ldap3.utils.dn import safe_dn
import os
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
from conf.local_settings_dev import *
else:
from conf.local_settings import *
"""
根据以下网站的说明
https://docs.microsoft.com/zh-cn/troubleshoot/windows/win32/change-windows-active-directory-user-password
密码存储在 unicodePwd 属性中的用户对象的 AD LDS 数据库中 此属性可以在受限条件下写入但无法读取 只能修改属性;无法在对象创建时或由搜索查询时添加它
为了修改此属性客户端必须具有到服务器的 128 位传输层安全性 (TLS) /Secure Socket Layer (SSL) 连接
使用 SSP 创建的会话密钥使用 NTLM Kerberos的加密会话也可接受只要达到最小密钥长度
若要使用 TLS/SSL 实现此连接
服务器必须拥有 128 RSA 连接的服务器证书
客户端必须信任生成服务器证书 (CA) 证书颁发机构
客户端和服务器都必须能够进行 128 位加密
unicodePwd 属性的语法为 octet-string;但是目录服务预期八进制字符串将包含 UNICODE 字符串 (因为属性的名称指示)
这意味着在 LDAP 中传递的此属性的任何值都必须是 BER 编码的 UNICODE 字符串 (基本编码规则) 八进制字符串
此外UNICODE 字符串必须以引号开头和结尾这些引号不是所需密码的一部分
可通过两种方法修改 unicodePwd 属性 第一种操作类似于正常的 用户更改密码 操作
在这种情况下修改请求必须同时包含删除和添加操作 删除操作必须包含当前密码并包含其周围的引号
添加操作必须包含所需的新密码其周围必须有引号
修改此属性的第二种方法类似于管理员重置用户密码 为此客户端必须以具有修改其他用户密码的足够权限的用户进行绑定
此修改请求应包含单个替换操作其中包含用引号括起的新所需密码 如果客户端具有足够的权限则无论旧密码是什么此密码都将变为新密码
"""
class AdOps(object):
def __init__(self, auto_bind=True, use_ssl=LDAP_USE_SSL, port=LDAP_CONN_PORT, domain=LDAP_DOMAIN, user=LDAP_LOGIN_USER,
password=LDAP_LOGIN_USER_PWD,
authentication=NTLM):
"""
AD连接器 authentication [SIMPLE, ANONYMOUS, SASL, NTLM]
:return:
"""
self.use_ssl = use_ssl
self.port = port
# 如果doamin\\user中doamin部分被写成域名格式 只提取DOMAIN部分
self.domain = domain.split('.')[0] if domain is not None else None
self.user = user
self.password = password
self.authentication = authentication
self.auto_bind = auto_bind
self.server = None
self.conn = None
def __server(self):
if self.server is None:
try:
self.server = Server(host='%s' % LDAP_HOST, connect_timeout=1, use_ssl=self.use_ssl, port=self.port,
get_info=ALL)
except LDAPInvalidCredentialsResult as lic_e:
return False, LDAPOperationResult("LDAPInvalidCredentialsResult: " + str(lic_e.message))
except LDAPOperationResult as lo_e:
return False, LDAPOperationResult("LDAPOperationResult: " + str(lo_e.message))
except LDAPException as l_e:
return False, LDAPException("LDAPException: " + str(l_e))
def __conn(self):
if self.conn is None:
try:
self.__server()
self.conn = Connection(self.server,
auto_bind=self.auto_bind, user=r'{}\{}'.format(self.domain, self.user),
password=self.password,
authentication=self.authentication,
raise_exceptions=True)
except LDAPInvalidCredentialsResult as lic_e:
return False, LDAPOperationResult("LDAPInvalidCredentialsResult: " + str(lic_e.message))
except LDAPOperationResult as lo_e:
return False, LDAPOperationResult("LDAPOperationResult: " + str(lo_e.message))
except LDAPException as l_e:
return False, LDAPException("LDAPException: " + str(l_e))
def ad_auth_user(self, username, password):
"""
验证账号
:param username:
:param password:
:return: True or False
"""
try:
self.__server()
c_auth = Connection(server=self.server, user=r'{}\{}'.format(self.domain, username), password=password,
auto_bind=True, raise_exceptions=True)
c_auth.unbind()
return True, '旧密码验证通过。'
except LDAPInvalidCredentialsResult as e:
if '52e' in e.message:
return False, u'账号或旧密码不正确!'
elif '775' in e.message:
return False, u'账号已锁定,请自行扫码解锁!'
elif '533' in e.message:
return False, u'账号已禁用!'
elif '525' in e.message:
return False, u'账号不存在!'
elif '532' in e.message:
return False, u'密码己过期!'
elif '701' in e.message:
return False, u'账号己过期!'
elif '773' in e.message:
# 如果仅仅使用普通凭据来绑定ldap用途请返回False, 让用户通过其他途径修改密码后再来验证登陆
# return False, '用户登陆前必须修改密码!'
# 设置该账号下次登陆不需要更改密码,再验证一次
self.__conn()
self.conn.search(search_base=BASE_DN, search_filter=SEARCH_FILTER.format(username),
attributes=['pwdLastSet'])
self.conn.modify(self.conn.entries[0].entry_dn, {'pwdLastSet': [(MODIFY_REPLACE, ['-1'])]})
return True, self.ad_auth_user(username, password)
else:
return False, u'旧密码认证失败,请确认账号的旧密码是否正确或使用重置密码功能。'
except LDAPException as e:
return False, "连接Ldap失败报错如下{}".format(e)
def ad_ensure_user_by_account(self, username):
"""
通过username查询某个用户是否在AD中
:param username:
:return: True or False
"""
try:
self.__conn()
return True, self.conn.search(BASE_DN, SEARCH_FILTER.format(username),
attributes=['sAMAccountName'])
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)
def ad_get_user_displayname_by_account(self, username):
"""
通过username查询某个用户的显示名
:param username:
:return: user_displayname
"""
try:
self.__conn()
self.conn.search(BASE_DN, SEARCH_FILTER.format(username), attributes=['name'])
return True, self.conn.entries[0]['name']
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)
def ad_get_user_dn_by_account(self, username):
"""
通过username查询某个用户的完整DN
:param username:
:return: DN
"""
try:
self.__conn()
self.conn.search(BASE_DN, SEARCH_FILTER.format(username),
attributes=['distinguishedName'])
return True, str(self.conn.entries[0]['distinguishedName'])
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)
def ad_get_user_status_by_account(self, username):
"""
通过username查询某个用户的账号状态
:param username:
:return: user_account_control code
"""
try:
self.__conn()
self.conn.search(BASE_DN, SEARCH_FILTER.format(username),
attributes=['userAccountControl'])
return True, self.conn.entries[0]['userAccountControl']
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)
def ad_unlock_user_by_account(self, username):
"""
通过username解锁某个用户
:param username:
:return:
"""
_status, user_dn = self.ad_get_user_dn_by_account(username)
if _status:
try:
return True, self.conn.extend.microsoft.unlock_account(user='%s' % user_dn)
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)
else:
return False, user_dn
def ad_reset_user_pwd_by_account(self, username, new_password):
"""
重置某个用户的密码
:param username:
:return:
"""
_status, user_dn = self.ad_get_user_dn_by_account(username)
if _status:
if self.conn.check_names:
user_dn = safe_dn(user_dn)
encoded_new_password = ('"%s"' % new_password).encode('utf-16-le')
result = self.conn.modify(user_dn,
{'unicodePwd': [(MODIFY_REPLACE, [encoded_new_password])]},
)
if not self.conn.strategy.sync:
_, result = self.conn.get_response(result)
else:
if self.conn.strategy.thread_safe:
_, result, _, _ = result
else:
result = self.conn.result
# change successful, returns True
if result['result'] == RESULT_SUCCESS:
return True, '密码己修改成功,请妥善保管!'
# change was not successful, raises exception if raise_exception = True in connection or returns the operation result, error code is in result['result']
if self.conn.raise_exceptions:
from ldap3.core.exceptions import LDAPOperationResult
_msg = LDAPOperationResult(result=result['result'], description=result['description'], dn=result['dn'],
message=result['message'],
response_type=result['type'])
return False, _msg
return False, result['result']
else:
return False, user_dn
def ad_get_user_locked_status_by_account(self, username):
"""
通过username获取某个用户账号是否被锁定
:param username:
:return: 如果结果是1601-01-01说明账号未锁定返回0
"""
try:
self.__conn()
self.conn.search(BASE_DN, SEARCH_FILTER.format(username),
attributes=['lockoutTime'])
locked_status = self.conn.entries[0]['lockoutTime']
if '1601-01-01' in str(locked_status):
return True, 'unlocked'
else:
return False, locked_status
except IndexError:
return False, "AdOps Exception: Connect.search未能检索到任何信息当前账号可能被排除在<SEARCH_FILTER>之外,请联系管理员处理。"
except Exception as e:
return False, "AdOps Exception: {}".format(e)

76
utils/dingding_ops.py Normal file
View File

@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from dingtalk.client import AppKeyClient
from pwdselfservice import cache_storage
import os
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
from conf.local_settings_dev import *
else:
from conf.local_settings import *
class DingDingOps(AppKeyClient):
def __init__(self, corp_id=DING_CORP_ID, app_key=DING_APP_KEY, app_secret=DING_APP_SECRET, mo_app_id=DING_MO_APP_ID,
mo_app_secret=DING_MO_APP_SECRET,
storage=cache_storage):
super().__init__(corp_id, app_key, app_secret, storage)
self.corp_id = corp_id
self.app_key = app_key
self.app_secret = app_secret
self.mo_app_id = mo_app_id
self.mo_app_secret = mo_app_secret
self.storage = storage
def get_user_id_by_code(self, code):
"""
通过code获取用户的 userid
:return:
"""
user_id_data = self.user.getuserinfo(code)
if user_id_data.get('errcode') == 0:
user_id = user_id_data.get('userid')
return True, user_id
else:
return False, '通过临时Code换取用户ID失败: %s' % str(user_id_data)
def get_user_detail_by_user_id(self, user_id):
"""
通过user_id 获取用户详细信息
user_id 用户ID
:return:
"""
try:
return True, self.user.get(user_id)
except Exception as e:
return False, 'get_user_detail_by_user_id: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'get_user_detail_by_user_id: %s' % str(k_error)
def get_user_detail(self, code, home_url):
"""
临时授权码换取userinfo
"""
_status, user_id = self.get_user_id_by_code(code)
# 判断 user_id 在本企业钉钉/微信中是否存在
if not _status:
context = {'global_title': TITLE,
'msg': '获取userid失败错误信息{}'.format(user_id),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return False, context, user_id
detail_status, user_info = self.get_user_detail_by_user_id(user_id)
if not detail_status:
context = {'global_title': TITLE,
'msg': '获取用户信息失败,错误信息:{}'.format(user_info),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回主页"
}
return False, context, user_info
return True, user_id, user_info

53
utils/format_username.py Normal file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @FileName format_username.py
# @Software:
# @Author: Leven Xiang
# @Mail: xiangle0109@outlook.com
# @Date 2021/4/19 9:17
import re
def get_email_from_userinfo(user_info):
if user_info.get('email') not in ['', None]:
return True, user_info.get('email')
elif user_info.get('biz_mail') not in ['', None]:
return True, user_info.get('biz_mail')
else:
return False, "当前用户的邮箱或企业邮箱均没配置,请先完善个人信息!"
def format2username(account):
"""
格式化账号统一输出为用户名格式
:param account 用户账号可以是邮箱DOMAIN\\usernameusername格式
:return: username
"""
if account is None:
return False, NameError(
"传入的用户账号为空!".format(account))
try:
mail_compile = re.compile(r'(.*)@(.*)')
domain_compile = re.compile(r'(.*)\\(.*)')
if re.fullmatch(mail_compile, account):
return True, re.fullmatch(mail_compile, account).group(1)
elif re.fullmatch(domain_compile, account):
return True, re.fullmatch(domain_compile, account).group(2)
else:
return True, account.lower()
except Exception as e:
return False, NameError("格式化失败注意account用户账号是邮箱或DOMAIN\\username或username格式错误信息[{}]".format(account, e))
def get_user_is_active(user_info):
try:
return True, user_info.get('active') or user_info.get('status')
except Exception as e:
return False, 'get_user_is_active: %s' % str(e)
except (KeyError, IndexError) as k_error:
return False, 'get_user_is_active: %s' % str(k_error)

42
utils/logger_filter.py Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import wraps
from traceback import format_exc
def decorator_request_logger(logger):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
try:
rsp = func(request, *args, **kwargs)
logger.info(
f'Request Arguments: {args} {kwargs}')
# logger.info(
# f'Request: {request.META["REMOTE_ADDR"]} {request.method} "{request.META["PATH_INFO"]}'
# f'{request.META["QUERY_STRING"]} {request.META["SERVER_PROTOCOL"]}" {rsp.status_code} {rsp.content}')
logger.info(rsp)
return rsp
except Exception as e:
logger.error(format_exc())
raise e
return wrapper
return decorator
def decorator_default_logger(logger):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
logger.info(f'{args}, {kwargs}')
try:
return func(*args, **kwargs)
except Exception as e:
logger.error(format_exc())
raise e
return wrapper
return decorator

23
utils/storage/__init__.py Normal file
View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
class BaseStorage(object):
def get(self, key, default=None):
raise NotImplementedError()
def set(self, key, value, ttl=None):
raise NotImplementedError()
def delete(self, key):
raise NotImplementedError()
def __getitem__(self, key):
self.get(key)
def __setitem__(self, key, value):
self.set(key, value)
def __delitem__(self, key):
self.delete(key)

62
utils/storage/cache.py Normal file
View File

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import inspect
from utils.storage import BaseStorage
def _is_cache_item(obj):
return isinstance(obj, CacheItem)
class CacheItem(object):
def __init__(self, cache=None, name=None):
self.cache = cache
self.name = name
def key_name(self, key):
if isinstance(key, (tuple, list)):
key = ':'.join(key)
k = '{0}:{1}'.format(self.cache.prefix, self.name)
if key is not None:
k = '{0}:{1}'.format(k, key)
return k
def get(self, key=None, default=None):
return self.cache.storage.get(self.key_name(key), default)
def set(self, key=None, value=None, ttl=None):
return self.cache.storage.set(self.key_name(key), value, ttl)
def delete(self, key=None):
return self.cache.storage.delete(self.key_name(key))
class BaseCache(object):
def __new__(cls, *args, **kwargs):
self = super(BaseCache, cls).__new__(cls)
api_endpoints = inspect.getmembers(self, _is_cache_item)
for name, api in api_endpoints:
api_cls = type(api)
api = api_cls(self, name)
setattr(self, name, api)
return self
def __init__(self, storage, prefix='client'):
assert isinstance(storage, BaseStorage)
self.storage = storage
self.prefix = prefix
class WeWorkCache(BaseCache):
access_token = CacheItem()
class DingDingCache(BaseCache):
access_token = CacheItem()

View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import json
import random
import string
import json
import six
from utils.storage import BaseStorage
def to_text(value, encoding='utf-8'):
"""Convert value to unicode, default encoding is utf-8
:param value: Value to be converted
:param encoding: Desired encoding
"""
if not value:
return ''
if isinstance(value, six.text_type):
return value
if isinstance(value, six.binary_type):
return value.decode(encoding)
return six.text_type(value)
def to_binary(value, encoding='utf-8'):
"""Convert value to binary string, default encoding is utf-8
:param value: Value to be converted
:param encoding: Desired encoding
"""
if not value:
return b''
if isinstance(value, six.binary_type):
return value
if isinstance(value, six.text_type):
return value.encode(encoding)
return to_text(value).encode(encoding)
def random_string(length=16):
rule = string.ascii_letters + string.digits
rand_list = random.sample(rule, length)
return ''.join(rand_list)
def byte2int(c):
if six.PY2:
return ord(c)
return c
class KvStorage(BaseStorage):
def __init__(self, kvdb, prefix='client'):
for method_name in ('get', 'set', 'delete'):
assert hasattr(kvdb, method_name)
self.kvdb = kvdb
self.prefix = prefix
def key_name(self, key):
return '{0}:{1}'.format(self.prefix, key)
def get(self, key, default=None):
key = self.key_name(key)
value = self.kvdb.get(key)
if value is None:
return default
return json.loads(to_text(value))
def set(self, key, value, ttl=None):
if value is None:
return
key = self.key_name(key)
value = json.dumps(value)
self.kvdb.set(key, value, ttl)
def delete(self, key):
key = self.key_name(key)
self.kvdb.delete(key)

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
import time
from utils.storage import BaseStorage
class MemoryStorage(BaseStorage):
def __init__(self):
self._data = {}
def get(self, key, default=None):
ret = self._data.get(key, None)
if ret is None or len(ret) != 2:
return default
else:
value = ret[0]
expires_at = ret[1]
if expires_at is None or expires_at > time.time():
return value
else:
return default
def set(self, key, value, ttl=3600):
if value is None:
return
self._data[key] = (value, int(time.time()) + ttl)
def delete(self, key):
self._data.pop(key, None)

View File

@ -0,0 +1,113 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import json
import requests
DEBUG = False
class ApiException(Exception):
def __init__(self, errCode, errMsg):
self.errCode = errCode
self.errMsg = errMsg
class AbstractApi(object):
def __init__(self):
return
def access_token(self):
raise NotImplementedError
def http_call(self, url_type, args=None):
short_url = url_type[0]
method = url_type[1]
response = {}
for retryCnt in range(0, 3):
if 'POST' == method:
url = self.__make_url(short_url)
response = self.__http_post(url, args)
elif 'GET' == method:
url = self.__make_url(short_url)
url = self.__append_args(url, args)
response = self.__http_get(url)
else:
raise ApiException(-1, "unknown method type")
# check if token expired
if self.__token_expired(response.get('errcode')):
self.__refresh_token(short_url)
retryCnt += 1
continue
else:
break
return self.__check_response(response)
@staticmethod
def __append_args(url, args):
if args is None:
return url
for key, value in args.items():
if '?' in url:
url += ('&' + key + '=' + value)
else:
url += ('?' + key + '=' + value)
return url
@staticmethod
def __make_url(short_url):
base = "https://qyapi.weixin.qq.com"
if short_url[0] == '/':
return base + short_url
else:
return base + '/' + short_url
def __append_token(self, url):
if 'ACCESS_TOKEN' in url:
return url.replace('ACCESS_TOKEN', self.access_token())
else:
return url
def __http_post(self, url, args):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url, args)
return requests.post(real_url, data=json.dumps(args, ensure_ascii=False).encode('utf-8')).json()
def __http_get(self, url):
real_url = self.__append_token(url)
if DEBUG is True:
print(real_url)
return requests.get(real_url).json()
def __post_file(self, url, media_file):
return requests.post(url, file=media_file).json()
@staticmethod
def __check_response(response):
errCode = response.get('errcode')
errMsg = response.get('errmsg')
if errCode == 0:
return response
else:
raise ApiException(errCode, errMsg)
@staticmethod
def __token_expired(errCode):
if errCode == 40014 or errCode == 42001 or errCode == 42007 or errCode == 42009:
return True
else:
return False
def __refresh_token(self, url):
if 'ACCESS_TOKEN' in url:
self.access_token()

191
utils/wework_ops.py Normal file
View File

@ -0,0 +1,191 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @FileName WEWORK_ops.py
# @Software:
# @Author: Leven Xiang
# @Mail: xiangle0109@outlook.com
# @Date 2021/5/18 16:55
from __future__ import absolute_import, unicode_literals
import os
from pwdselfservice import cache_storage
from utils.storage.cache import WeWorkCache
from utils.wework_api.abstract_api import *
APP_ENV = os.getenv('APP_ENV')
if APP_ENV == 'dev':
from conf.local_settings_dev import *
else:
from conf.local_settings import *
CORP_API_TYPE = {
'GET_USER_TICKET_OAUTH2': ['/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_INFO_OAUTH2': ['/cgi-bin/auth/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
'GET_ACCESS_TOKEN': ['/cgi-bin/gettoken', 'GET'],
'USER_CREATE': ['/cgi-bin/user/create?access_token=ACCESS_TOKEN', 'POST'],
'USER_GET': ['/cgi-bin/user/get?access_token=ACCESS_TOKEN', 'GET'],
'USER_UPDATE': ['/cgi-bin/user/update?access_token=ACCESS_TOKEN', 'POST'],
'USER_DELETE': ['/cgi-bin/user/delete?access_token=ACCESS_TOKEN', 'GET'],
'USER_BATCH_DELETE': ['/cgi-bin/user/batchdelete?access_token=ACCESS_TOKEN', 'POST'],
'USER_SIMPLE_LIST': ['/cgi-bin/user/simplelist?access_token=ACCESS_TOKEN', 'GET'],
'USER_LIST': ['/cgi-bin/user/list?access_token=ACCESS_TOKEN', 'GET'],
'USERID_TO_OPENID': ['/cgi-bin/user/convert_to_openid?access_token=ACCESS_TOKEN', 'POST'],
'OPENID_TO_USERID': ['/cgi-bin/user/convert_to_userid?access_token=ACCESS_TOKEN', 'POST'],
'USER_AUTH_SUCCESS': ['/cgi-bin/user/authsucc?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_CREATE': ['/cgi-bin/department/create?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_UPDATE': ['/cgi-bin/department/update?access_token=ACCESS_TOKEN', 'POST'],
'DEPARTMENT_DELETE': ['/cgi-bin/department/delete?access_token=ACCESS_TOKEN', 'GET'],
'DEPARTMENT_LIST': ['/cgi-bin/department/list?access_token=ACCESS_TOKEN', 'GET'],
'TAG_CREATE': ['/cgi-bin/tag/create?access_token=ACCESS_TOKEN', 'POST'],
'TAG_UPDATE': ['/cgi-bin/tag/update?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE': ['/cgi-bin/tag/delete?access_token=ACCESS_TOKEN', 'GET'],
'TAG_GET_USER': ['/cgi-bin/tag/get?access_token=ACCESS_TOKEN', 'GET'],
'TAG_ADD_USER': ['/cgi-bin/tag/addtagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_DELETE_USER': ['/cgi-bin/tag/deltagusers?access_token=ACCESS_TOKEN', 'POST'],
'TAG_GET_LIST': ['/cgi-bin/tag/list?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_JOB_GET_RESULT': ['/cgi-bin/batch/getresult?access_token=ACCESS_TOKEN', 'GET'],
'BATCH_INVITE': ['/cgi-bin/batch/invite?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET': ['/cgi-bin/agent/get?access_token=ACCESS_TOKEN', 'GET'],
'AGENT_SET': ['/cgi-bin/agent/set?access_token=ACCESS_TOKEN', 'POST'],
'AGENT_GET_LIST': ['/cgi-bin/agent/list?access_token=ACCESS_TOKEN', 'GET'],
'MENU_CREATE': ['/cgi-bin/menu/create?access_token=ACCESS_TOKEN', 'POST'],
'MENU_GET': ['/cgi-bin/menu/get?access_token=ACCESS_TOKEN', 'GET'],
'MENU_DELETE': ['/cgi-bin/menu/delete?access_token=ACCESS_TOKEN', 'GET'],
'MESSAGE_SEND': ['/cgi-bin/message/send?access_token=ACCESS_TOKEN', 'POST'],
'MESSAGE_REVOKE': ['/cgi-bin/message/revoke?access_token=ACCESS_TOKEN', 'POST'],
'MEDIA_GET': ['/cgi-bin/media/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_INFO_BY_CODE': ['/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN', 'GET'],
'GET_USER_DETAIL': ['/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN', 'POST'],
'GET_TICKET': ['/cgi-bin/ticket/get?access_token=ACCESS_TOKEN', 'GET'],
'GET_JSAPI_TICKET': ['/cgi-bin/get_jsapi_ticket?access_token=ACCESS_TOKEN', 'GET'],
'GET_CHECKIN_OPTION': ['/cgi-bin/checkin/getcheckinoption?access_token=ACCESS_TOKEN', 'POST'],
'GET_CHECKIN_DATA': ['/cgi-bin/checkin/getcheckindata?access_token=ACCESS_TOKEN', 'POST'],
'GET_APPROVAL_DATA': ['/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN', 'POST'],
'GET_INVOICE_INFO': ['/cgi-bin/card/invoice/reimburse/getinvoiceinfo?access_token=ACCESS_TOKEN', 'POST'],
'UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updateinvoicestatus?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_UPDATE_INVOICE_STATUS':
['/cgi-bin/card/invoice/reimburse/updatestatusbatch?access_token=ACCESS_TOKEN', 'POST'],
'BATCH_GET_INVOICE_INFO':
['/cgi-bin/card/invoice/reimburse/getinvoiceinfobatch?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_CREATE': ['/cgi-bin/appchat/create?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_GET': ['/cgi-bin/appchat/get?access_token=ACCESS_TOKEN', 'GET'],
'APP_CHAT_UPDATE': ['/cgi-bin/appchat/update?access_token=ACCESS_TOKEN', 'POST'],
'APP_CHAT_SEND': ['/cgi-bin/appchat/send?access_token=ACCESS_TOKEN', 'POST'],
'MINIPROGRAM_CODE_TO_SESSION_KEY': ['/cgi-bin/miniprogram/jscode2session?access_token=ACCESS_TOKEN', 'GET'],
}
class WeWorkOps(AbstractApi):
def __init__(self, corp_id=WEWORK_CORP_ID, agent_id=WEWORK_AGENT_ID, agent_secret=WEWORK_AGNET_SECRET,
storage=cache_storage, prefix='wework'):
super().__init__()
self.corp_id = corp_id
self.agent_id = agent_id
self.agent_secret = agent_secret
self.storage = storage
self.cache = WeWorkCache(self.storage, "%s:%s" % (prefix, "corp_id:%s" % self.corp_id))
def access_token(self):
access_token = self.cache.access_token.get()
if access_token is None:
ret = self.get_access_token()
access_token = ret['access_token']
expires_in = ret.get('expires_in', 7200)
self.cache.access_token.set(value=access_token, ttl=expires_in)
return access_token
def get_access_token(self):
return self.http_call(
CORP_API_TYPE['GET_ACCESS_TOKEN'],
{
'corpid': self.corp_id,
'corpsecret': self.agent_secret,
})
def get_user_id_by_code(self, code):
try:
return True, self.http_call(
CORP_API_TYPE['GET_USER_INFO_BY_CODE'],
{
'code': code,
}).get('UserId')
except ApiException as e:
return False, "get_user_id_by_code: {}-{}".format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_id_by_code: {}".format(e)
def get_user_detail_by_user_id(self, user_id):
try:
return True, self.http_call(
CORP_API_TYPE['USER_GET'],
{
'userid': user_id,
})
except ApiException as e:
return False, "get_user_detail_by_user_id: {}-{}".format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_detail_by_user_id: {}".format(e)
def get_user_ticket_by_code_with_oauth2(self, code):
try:
return True, self.http_call(
CORP_API_TYPE['GET_USER_TICKET_OAUTH2'],
{
'code': code,
})
except ApiException as e:
return False, "get_user_ticket_by_code_with_oauth2: {}-{}".format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_ticket_by_code_with_oauth2: {}".format(e)
def get_user_info_by_ticket_with_oauth2(self, user_ticket):
try:
return True, self.http_call(
CORP_API_TYPE['GET_USER_INFO_OAUTH2'],
{
'user_ticket': user_ticket
})
except ApiException as e:
return False, "get_user_info_by_ticket_with_oauth2: {}-{}".format(e.errCode, e.errMsg)
except Exception as e:
return False, "get_user_info_by_ticket_with_oauth2: {}".format(e)
def get_user_detail(self, code, home_url):
"""
临时授权码换取userinfo
"""
_status, ticket_data = self.get_user_ticket_by_code_with_oauth2(code)
# 判断 user_ticket 是否存在
if not _status:
context = {'global_title': TITLE,
'msg': '获取userid失败错误信息{}'.format(ticket_data),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return False, context, ticket_data
user_id = ticket_data.get('userid')
if ticket_data.get('user_ticket') is None:
context = {'global_title': TITLE,
'msg': '获取用户Ticket失败当前扫码用户[{}]可能未加入企业!'.format(user_id),
'button_click': "window.location.href='%s'" % home_url,
'button_display': "返回修改密码"
}
return False, context, user_id
# 通过user_ticket获取企业微信用户详情信息
detail_status, user_info = self.get_user_info_by_ticket_with_oauth2(ticket_data.get('user_ticket'))
print("get_user_info_by_ticket_with_oauth2 --- ", user_info)
if not detail_status:
context = {'global_title': TITLE,
'msg': '获取用户信息失败,错误信息:{}'.format(user_id),
'button_click': "window.location.href='%s'" % '/auth',
'button_display': "重新认证授权"
}
return False, context
return True, user_id, user_info

30
uwsgi.ini Normal file
View File

@ -0,0 +1,30 @@
[uwsgi]
http-socket = PWD_SELF_SERVICE_IP:PWD_SELF_SERVICE_PORT
chdir = PWD_SELF_SERVICE_HOME
env=DJANGO_SETTINGS_MODULE=pwdselfservice.settings
module = pwdselfservice.wsgi:application
master = true
processes = CPU_NUM
threads = CPU_NUM
max-requests = 2000
chmod-socket = 755
vacuum = true
#设置缓冲
post-buffering = 4096
#设置静态文件
static-map = /static=PWD_SELF_SERVICE_HOME/static
#设置日志目录
daemonize = PWD_SELF_SERVICE_HOME/log/uwsgi.log

53
uwsgiserver Normal file
View File

@ -0,0 +1,53 @@
#!/bin/sh
# Startup script for the uwsgi server
# chkconfig: - 85 15
# description: uwsgi server is Web Server
# HTML files and CGI.
# processname: uwsgiserver
INI="PWD_SELF_SERVICE_HOME/uwsgi.ini"
UWSGI="PYTHON_INSTALL_DIR/bin/uwsgi"
PSID="ps aux | grep "uwsgi"| grep -v "grep" | wc -l"
if [ ! -n "$1" ]
then
content="Usages: sh uwsgiserver [start|stop|restart|status]"
echo -e "\033[31m $content \033[0m"
exit 0
fi
if [ $1 = start ]
then
if [ `eval $PSID` -gt 4 ]
then
content="uwsgi is running!"
echo -e "\033[32m $content \033[0m"
exit 0
else
$UWSGI $INI
content="Start uwsgi service [OK]"
echo -e "\033[32m $content \033[0m"
fi
elif [ $1 = stop ];then
if [ `eval $PSID` -gt 4 ];then
killall -9 uwsgi
fi
content="Stop uwsgi service [OK]"
echo -e "\033[32m $content \033[0m"
elif [ $1 = restart ];then
if [ `eval $PSID` -gt 4 ];then
killall -9 uwsgi
fi
$UWSGI --ini $INI
content="Restart uwsgi service [OK]"
echo -e "\033[32m $content \033[0m"
elif [ $1 = status ];then
ps -ef | grep uwsgi | grep -v "uwsgiserver" | grep -v "grep"
else
content="Usages: sh uwsgiserver [start|stop|restart|status]"
echo -e "\033[31m $content \033[0m"
fi