From 9424c2fcbcff422038127fbe43871a6df15e3212 Mon Sep 17 00:00:00 2001 From: root <root@localhost.localdomain> Date: Fri, 2 Aug 2019 17:13:22 +0800 Subject: [PATCH] =?UTF-8?q?ver1.2.0=20=E5=AE=8C=E5=96=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?;=E6=96=B0=E5=A2=9E=20docker=20=E6=96=B9=E5=BC=8F=E9=83=A8?= =?UTF-8?q?=E7=BD=B2;=E5=B0=9D=E8=AF=95=E5=8A=A0=E5=85=A5=20celery=204.3.0?= =?UTF-8?q?=20=E5=AE=9E=E7=8E=B0=E5=BC=82=E6=AD=A5=E4=BB=BB=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/inspectionProfiles/Project_Default.xml | 15 + .idea/workspace.xml | 224 +++++++++---- Dockerfile | 8 + README.md | 12 +- db.sqlite3 | Bin 10215424 -> 10215424 bytes deamon.ini | 28 ++ devops/__init__.py | 4 + devops/__pycache__/__init__.cpython-37.pyc | Bin 132 -> 282 bytes devops/__pycache__/celery.cpython-37.pyc | Bin 0 -> 468 bytes devops/__pycache__/settings.cpython-37.pyc | Bin 2591 -> 3306 bytes devops/__pycache__/urls.cpython-37.pyc | Bin 1124 -> 1353 bytes devops/celery.py | 16 + devops/settings.py | 29 +- devops/urls.py | 19 +- requirements.txt | 6 + run.bat | 1 + run_celery_work.bat | 2 + server/__pycache__/tasks.cpython-37.pyc | Bin 0 -> 394 bytes server/__pycache__/urls.cpython-37.pyc | Bin 361 -> 357 bytes server/__pycache__/urls_api.cpython-37.pyc | Bin 0 -> 294 bytes server/__pycache__/views.cpython-37.pyc | Bin 1004 -> 1194 bytes server/__pycache__/views_api.cpython-37.pyc | Bin 0 -> 454 bytes server/tasks.py | 14 + server/urls.py | 5 +- server/urls_api.py | 8 + server/views.py | 17 +- server/views_api.py | 12 + start_docker.sh | 7 + templates/base.html | 118 +------ templates/server/hosts.html | 144 +++++++++ templates/server/users.html | 118 +++++++ templates/user/groups.html | 113 +++++++ templates/user/logs.html | 119 +++++++ templates/user/profile.html | 100 ++++++ templates/user/profile_edit.html | 130 ++++++++ templates/user/user.html | 102 ++++++ templates/user/user_edit.html | 218 +++++++++++++ templates/user/users.html | 140 +++++++++ templates/webssh/hosts.html | 162 ++++++++++ templates/webssh/logs.html | 134 ++++++++ upload_to_server.py | 30 ++ user/__pycache__/forms.cpython-37.pyc | Bin 859 -> 2238 bytes user/__pycache__/models.cpython-37.pyc | Bin 3866 -> 3981 bytes user/__pycache__/urls.cpython-37.pyc | Bin 538 -> 620 bytes user/__pycache__/urls_api.cpython-37.pyc | Bin 0 -> 414 bytes user/__pycache__/views.cpython-37.pyc | Bin 5000 -> 4327 bytes user/__pycache__/views_api.cpython-37.pyc | Bin 0 -> 4490 bytes user/forms.py | 37 +++ user/migrations/0004_auto_20190801_1537.py | 28 ++ .../0004_auto_20190801_1537.cpython-37.pyc | Bin 0 -> 718 bytes user/models.py | 3 + user/urls.py | 31 +- user/urls_api.py | 10 + user/views.py | 296 ++++++++---------- user/views_api.py | 157 ++++++++++ webssh/__pycache__/urls.cpython-37.pyc | Bin 364 -> 364 bytes webssh/__pycache__/views.cpython-37.pyc | Bin 1215 -> 1401 bytes webssh/__pycache__/websocket.cpython-37.pyc | Bin 4757 -> 5023 bytes webssh/urls.py | 4 +- webssh/views.py | 14 +- webssh/websocket.py | 12 + webtelnet/__pycache__/urls.cpython-37.pyc | Bin 300 -> 300 bytes webtelnet/__pycache__/views.cpython-37.pyc | Bin 868 -> 868 bytes .../__pycache__/websocket.cpython-37.pyc | Bin 4125 -> 4395 bytes webtelnet/urls.py | 2 +- webtelnet/websocket.py | 12 + 66 files changed, 2298 insertions(+), 363 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 Dockerfile create mode 100644 deamon.ini create mode 100644 devops/__pycache__/celery.cpython-37.pyc create mode 100644 devops/celery.py create mode 100644 run_celery_work.bat create mode 100644 server/__pycache__/tasks.cpython-37.pyc create mode 100644 server/__pycache__/urls_api.cpython-37.pyc create mode 100644 server/__pycache__/views_api.cpython-37.pyc create mode 100644 server/tasks.py create mode 100644 server/urls_api.py create mode 100644 server/views_api.py create mode 100644 start_docker.sh create mode 100644 templates/server/hosts.html create mode 100644 templates/server/users.html create mode 100644 templates/user/groups.html create mode 100644 templates/user/logs.html create mode 100644 templates/user/profile.html create mode 100644 templates/user/profile_edit.html create mode 100644 templates/user/user.html create mode 100644 templates/user/user_edit.html create mode 100644 templates/user/users.html create mode 100644 templates/webssh/hosts.html create mode 100644 templates/webssh/logs.html create mode 100644 upload_to_server.py create mode 100644 user/__pycache__/urls_api.cpython-37.pyc create mode 100644 user/__pycache__/views_api.cpython-37.pyc create mode 100644 user/migrations/0004_auto_20190801_1537.py create mode 100644 user/migrations/__pycache__/0004_auto_20190801_1537.cpython-37.pyc create mode 100644 user/urls_api.py create mode 100644 user/views_api.py diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..378a7e8 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,15 @@ +<component name="InspectionProjectProfileManager"> + <profile version="1.0"> + <option name="myName" value="Project Default" /> + <inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true"> + <option name="ignoredPackages"> + <value> + <list size="2"> + <item index="0" class="java.lang.String" itemvalue="simpleui" /> + <item index="1" class="java.lang.String" itemvalue="supervisor" /> + </list> + </value> + </option> + </inspection_tool> + </profile> +</component> \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 4db7773..3241d0e 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -11,22 +11,52 @@ <component name="FileEditorManager"> <leaf SIDE_TABS_SIZE_LIMIT_KEY="300"> <file pinned="false" current-in-tab="false"> - <entry file="file://$PROJECT_DIR$/server/views.py"> + <entry file="file://$PROJECT_DIR$/user/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="345"> + <caret line="16" column="77" selection-start-line="16" selection-start-column="74" selection-end-line="16" selection-end-column="77" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/user/views.py"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="253"> - <caret line="11" column="15" selection-start-line="11" selection-start-column="4" selection-end-line="11" selection-end-column="15" /> + <caret line="216" column="19" selection-start-line="216" selection-start-column="4" selection-end-line="216" selection-end-column="19" /> <folding> - <element signature="e#0#35#0" expanded="true" /> + <element signature="e#0#64#0" expanded="true" /> </folding> </state> </provider> </entry> </file> <file pinned="false" current-in-tab="true"> - <entry file="file://$PROJECT_DIR$/server/models.py"> + <entry file="file://$PROJECT_DIR$/user/urls.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="437"> + <caret line="19" selection-start-line="19" selection-end-line="19" /> + <folding> + <element signature="e#0#28#0" expanded="true" /> + </folding> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/user/forms.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="552"> + <caret line="33" column="20" selection-start-line="33" selection-start-column="6" selection-end-line="33" selection-end-column="20" /> + </state> + </provider> + </entry> + </file> + <file pinned="false" current-in-tab="false"> + <entry file="file://$PROJECT_DIR$/templates/user/user_update.html"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="782"> - <caret line="40" column="22" selection-start-line="40" selection-start-column="22" selection-end-line="40" selection-end-column="22" /> + <state relative-caret-position="469"> + <caret line="84" column="48" selection-start-line="84" selection-start-column="48" selection-end-line="84" selection-end-column="48" /> </state> </provider> </entry> @@ -42,15 +72,11 @@ <component name="IdeDocumentHistory"> <option name="CHANGED_PATHS"> <list> - <option value="$PROJECT_DIR$/user/models.py" /> <option value="$PROJECT_DIR$/webssh/forms.py" /> <option value="$PROJECT_DIR$/static/webssh/webssh.js" /> <option value="$PROJECT_DIR$/webssh/urls.py" /> <option value="$PROJECT_DIR$/webssh/admin.py" /> <option value="$PROJECT_DIR$/webssh/views.py" /> - <option value="$PROJECT_DIR$/user/forms.py" /> - <option value="$PROJECT_DIR$/user/urls.py" /> - <option value="$PROJECT_DIR$/user/views.py" /> <option value="$PROJECT_DIR$/webssh/models.py" /> <option value="$PROJECT_DIR$/webssh/routing.py" /> <option value="$PROJECT_DIR$/devops/routing.py" /> @@ -60,12 +86,20 @@ <option value="$PROJECT_DIR$/webtelnet/websocket.py" /> <option value="$PROJECT_DIR$/server/models.py" /> <option value="$PROJECT_DIR$/server/views.py" /> + <option value="$PROJECT_DIR$/upload_to_server.py" /> + <option value="$PROJECT_DIR$/user/models.py" /> + <option value="$PROJECT_DIR$/templates/user/user_lists.html" /> + <option value="$PROJECT_DIR$/templates/user/user_info.html" /> + <option value="$PROJECT_DIR$/user/forms.py" /> + <option value="$PROJECT_DIR$/user/views.py" /> + <option value="$PROJECT_DIR$/templates/user/user_update.html" /> + <option value="$PROJECT_DIR$/user/urls.py" /> </list> </option> </component> <component name="ProjectFrameBounds" extendedState="6"> - <option name="x" value="380" /> - <option name="y" value="61" /> + <option name="x" value="411" /> + <option name="y" value="-16" /> <option name="width" value="1940" /> <option name="height" value="1100" /> </component> @@ -85,7 +119,18 @@ <path> <item name="devops" type="b2602c69:ProjectViewProjectNode" /> <item name="devops" type="462c0819:PsiDirectoryNode" /> - <item name="server" type="462c0819:PsiDirectoryNode" /> + <item name="templates" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="devops" type="b2602c69:ProjectViewProjectNode" /> + <item name="devops" type="462c0819:PsiDirectoryNode" /> + <item name="templates" type="462c0819:PsiDirectoryNode" /> + <item name="user" type="462c0819:PsiDirectoryNode" /> + </path> + <path> + <item name="devops" type="b2602c69:ProjectViewProjectNode" /> + <item name="devops" type="462c0819:PsiDirectoryNode" /> + <item name="user" type="462c0819:PsiDirectoryNode" /> </path> </expand> <select /> @@ -108,6 +153,34 @@ </list> </option> </component> + <component name="RunManager"> + <configuration name="upload_to_server" type="PythonConfigurationType" factoryName="Python" temporary="true"> + <module name="devops" /> + <option name="INTERPRETER_OPTIONS" value="" /> + <option name="PARENT_ENVS" value="true" /> + <envs> + <env name="PYTHONUNBUFFERED" value="1" /> + </envs> + <option name="SDK_HOME" value="" /> + <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> + <option name="IS_MODULE_SDK" value="true" /> + <option name="ADD_CONTENT_ROOTS" value="true" /> + <option name="ADD_SOURCE_ROOTS" value="true" /> + <option name="SCRIPT_NAME" value="$PROJECT_DIR$/upload_to_server.py" /> + <option name="PARAMETERS" value="" /> + <option name="SHOW_COMMAND_LINE" value="false" /> + <option name="EMULATE_TERMINAL" value="false" /> + <option name="MODULE_MODE" value="false" /> + <option name="REDIRECT_INPUT" value="false" /> + <option name="INPUT_FILE" value="" /> + <method v="2" /> + </configuration> + <recent_temporary> + <list> + <item itemvalue="Python.upload_to_server" /> + </list> + </recent_temporary> + </component> <component name="SvnConfiguration"> <configuration /> </component> @@ -129,8 +202,8 @@ <window_info id="Favorites" order="2" side_tool="true" /> <window_info anchor="bottom" id="Message" order="0" /> <window_info anchor="bottom" id="Find" order="1" /> - <window_info anchor="bottom" id="Run" order="2" /> - <window_info anchor="bottom" id="Debug" order="3" weight="0.4" /> + <window_info anchor="bottom" id="Run" order="2" weight="0.29112554" /> + <window_info anchor="bottom" id="Debug" order="3" weight="0.39935064" /> <window_info anchor="bottom" id="Cvs" order="4" weight="0.25" /> <window_info anchor="bottom" id="Inspection" order="5" weight="0.4" /> <window_info anchor="bottom" id="TODO" order="6" /> @@ -185,13 +258,6 @@ </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/user/forms.py"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="299"> - <caret line="13" column="22" selection-start-line="13" selection-start-column="6" selection-end-line="13" selection-end-column="22" /> - </state> - </provider> - </entry> <entry file="file://$PROJECT_DIR$/webssh/views.py"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="552"> @@ -209,33 +275,6 @@ </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/user/models.py"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="207"> - <caret line="10" column="5" selection-start-line="10" selection-start-column="5" selection-end-line="10" selection-end-column="5" /> - </state> - </provider> - </entry> - <entry file="file://$PROJECT_DIR$/user/urls.py"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="276"> - <caret line="12" column="53" selection-start-line="12" selection-start-column="45" selection-end-line="12" selection-end-column="53" /> - <folding> - <element signature="e#0#28#0" expanded="true" /> - </folding> - </state> - </provider> - </entry> - <entry file="file://$PROJECT_DIR$/user/views.py"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="828"> - <caret line="158" selection-start-line="158" selection-end-line="158" /> - <folding> - <element signature="e#0#45#0" expanded="true" /> - </folding> - </state> - </provider> - </entry> <entry file="file://C:/Program Files/Python37/Lib/site-packages/channels/routing.py"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="452"> @@ -250,16 +289,7 @@ </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/webssh/routing.py"> - <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="184"> - <caret line="8" lean-forward="true" selection-end-line="8" /> - <folding> - <element signature="e#0#28#0" expanded="true" /> - </folding> - </state> - </provider> - </entry> + <entry file="file://$PROJECT_DIR$/webssh/routing.py" /> <entry file="file://$PROJECT_DIR$/websocket/routing.py"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="184"> @@ -343,20 +373,86 @@ </state> </provider> </entry> + <entry file="file://$PROJECT_DIR$/server/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="782"> + <caret line="40" column="22" selection-start-line="40" selection-start-column="22" selection-end-line="40" selection-end-column="22" /> + </state> + </provider> + </entry> <entry file="file://$PROJECT_DIR$/server/views.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="184"> + <caret line="12" column="15" selection-start-line="12" selection-start-column="15" selection-end-line="12" selection-end-column="15" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/upload_to_server.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="138"> + <caret line="6" column="14" selection-start-line="6" selection-start-column="14" selection-end-line="6" selection-end-column="14" /> + </state> + </provider> + </entry> + <entry file="file://C:/Program Files/Python37/Lib/site-packages/paramiko/sftp_client.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="383"> + <caret line="696" column="49" selection-start-line="696" selection-start-column="41" selection-end-line="696" selection-end-column="49" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/templates/user/user_lists.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="506"> + <caret line="55" column="40" selection-start-line="55" selection-start-column="40" selection-end-line="55" selection-end-column="40" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/templates/user/user_info.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="414"> + <caret line="33" column="22" selection-start-line="33" selection-start-column="22" selection-end-line="33" selection-end-column="22" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/user/models.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="345"> + <caret line="16" column="77" selection-start-line="16" selection-start-column="74" selection-end-line="16" selection-end-column="77" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/templates/user/user_update.html"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="469"> + <caret line="84" column="48" selection-start-line="84" selection-start-column="48" selection-end-line="84" selection-end-column="48" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/user/forms.py"> + <provider selected="true" editor-type-id="text-editor"> + <state relative-caret-position="552"> + <caret line="33" column="20" selection-start-line="33" selection-start-column="6" selection-end-line="33" selection-end-column="20" /> + </state> + </provider> + </entry> + <entry file="file://$PROJECT_DIR$/user/views.py"> <provider selected="true" editor-type-id="text-editor"> <state relative-caret-position="253"> - <caret line="11" column="15" selection-start-line="11" selection-start-column="4" selection-end-line="11" selection-end-column="15" /> + <caret line="216" column="19" selection-start-line="216" selection-start-column="4" selection-end-line="216" selection-end-column="19" /> <folding> - <element signature="e#0#35#0" expanded="true" /> + <element signature="e#0#64#0" expanded="true" /> </folding> </state> </provider> </entry> - <entry file="file://$PROJECT_DIR$/server/models.py"> + <entry file="file://$PROJECT_DIR$/user/urls.py"> <provider selected="true" editor-type-id="text-editor"> - <state relative-caret-position="782"> - <caret line="40" column="22" selection-start-line="40" selection-start-column="22" selection-end-line="40" selection-end-column="22" /> + <state relative-caret-position="437"> + <caret line="19" selection-start-line="19" selection-end-line="19" /> + <folding> + <element signature="e#0#28#0" expanded="true" /> + </folding> </state> </provider> </entry> diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a5e91d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.7 +ADD . /devops +WORKDIR /devops +RUN cd /devops && pip install -i https://mirrors.aliyun.com/pypi/simple/ -r requirements.txt +RUN cd /devops && echo_supervisord_conf > /etc/supervisord.conf && cat deamon.ini >> /etc/supervisord.conf && \ + sed -i 's/nodaemon=false/nodaemon=true/g' /etc/supervisord.conf +EXPOSE 8000 +ENTRYPOINT ["supervisord", "-c", "/etc/supervisord.conf"] diff --git a/README.md b/README.md index 93a476c..f400b3b 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ # 安装 +原始方式 ``` # 安装相关库 pip install -r requirements.txt @@ -11,6 +12,11 @@ pip install -r requirements.txt python3 manage.py runserver ``` +docker方式(centos7) +``` +sh start_docker.sh +``` + 访问首页:http://127.0.0.1:8000 账号: admin 密码:123456 @@ -29,15 +35,15 @@ python3 manage.py runserver # TODO LISTS - [x] 用户登陆 -- [ ] 查看用户信息 -- [ ] 修改用户信息 +- [x] 查看用户信息 +- [x] 修改用户信息 - [x] 修改用户密码 - [ ] 重置用户密码 - [x] 查看用户 - [x] 查看用户组 - [ ] 添加用户 - [ ] 添加用户组 -- [ ] 修改用户 +- [x] 修改用户 - [ ] 修改用户组 - [ ] 删除用户 - [ ] 删除用户组 diff --git a/db.sqlite3 b/db.sqlite3 index b9c49e4a5cd88fe260d81d7797f1892836b0c48e..3605b8ff6e520cf3aa1af19a445818e76bf0c3a6 100644 GIT binary patch delta 20424 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 delta 6103 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 Ezpdz47XSbN diff --git a/deamon.ini b/deamon.ini new file mode 100644 index 0000000..c7b9b22 --- /dev/null +++ b/deamon.ini @@ -0,0 +1,28 @@ + +[program:devops] +directory=/devops +command=python manage.py runserver 0.0.0.0:8000 +# stdout_logfile=/var/log/celeryd.log +# stderr_logfile=/var/log/celeryd.log +loglevel=info +redirect_stderr=true +user = root +stopsignal = INT +autostart=true +autorestart=true +startsecs=10 +stopwaitsecs=600 + +[program:celery-worker] +directory=/devops +command=celery -A devops worker -l info +# stdout_logfile=/var/log/celeryd.log +# stderr_logfile=/var/log/celeryd.log +loglevel=info +redirect_stderr=true +user = root +stopsignal = INT +autostart=true +autorestart=true +startsecs=10 +stopwaitsecs=600 diff --git a/devops/__init__.py b/devops/__init__.py index e69de29..0be809d 100644 --- a/devops/__init__.py +++ b/devops/__init__.py @@ -0,0 +1,4 @@ +from __future__ import absolute_import, unicode_literals +from .celery import app as celery_app + +__all__ = ['celery_app'] diff --git a/devops/__pycache__/__init__.cpython-37.pyc b/devops/__pycache__/__init__.cpython-37.pyc index eefe22f22b89bb5e83156a9faa6f8455004533a9..88335a65fbb1a1ea0c77dc0efad18f5ed68fb54e 100644 GIT binary patch literal 282 zcmXwz%}N6?6or$_Ps?DjPa({rZd?c=Dh0Q0+=Z6QbP_d^OhSHy@r`_?Y+dygTzNz3 zf&1Na4`*?=t5ru(`^WA68~1NH&SGeuaKaTr5J5FDlvBevqeL)K>B`i+L0`X9-iqdx z^z9M5q-XhZI0UOxl%R~WKIV0r)M$kSt)ldU4o6y=G*^Rjxf`XHeg^a}{epJ@Zz(1( z0epGGR|wvoO1>#ci^vOrL2G~?;+G{X_}e_+?c5wc>~wPr&f8Bp#+{H~)`jx|P?L%P V*KW?sdt*h?@}c|_YQ&meuz#PnNiF~Y delta 76 zcmbQm)WT@*#LLUY00g14tztp+V-NuYj6jA15Erumi4=xl22Do4l?+87VJI>2q9aI- F0RVXB3BdpW diff --git a/devops/__pycache__/celery.cpython-37.pyc b/devops/__pycache__/celery.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1711a4db30f4709132b50abaed2202a565f00f54 GIT binary patch literal 468 zcmYjO%}N6?5KeZt+wE#qiuevi=*5E|;{PIwR<QNpqJ+4a)~(sil4J|*)wA!QC-IeX z_2esfGO-8_%$NDeeBY29wc9bWwzGe9|Bey*bjhCuHaM`Zh87rNn4<!xxbRXByWHb` z5v0Ly?DIz2fW{<zZL)wh-s}wsSon<72%<@gHCe=3FRmKB#&5{Z_yg39zbrHAdS;bA zD8XwDBrhr<wOOmnJQEBE&oxNORTrD^7&ypT-)x+mANEhL$RHUG&-$kWa(Q)fbCJwf z8B9f`dJ448%dtwsMPa_qCbS%jUM9-XzwU8zkzC(e=2l4yP!-LfyJTWQMzyXb5F$Oh zp7br4F#8pk?oo^<lxada&7~+!Y~>h?Xw9|pX;q~iCzFrKNQ#1phY4gl?a*2ama9xm zK@v^Xllnli?dTnRZzel;)l5H%a%-DZQn;bJ#rA*4Vu4;YGtq7#Sj}P2{uolr7S`}G Jj{Fth`42G6hwT6W literal 0 HcmV?d00001 diff --git a/devops/__pycache__/settings.cpython-37.pyc b/devops/__pycache__/settings.cpython-37.pyc index 54cca21bdd8f50f028ba2f1f3a63e1dabb037b73..db35ed19fbc3a8f8cf668f5be49be233adaa0e6d 100644 GIT binary patch delta 1076 zcmaKrO-~a+7{@!y(pse|P+sL-5h}8!pdg6S7OSEgsBJNE60%wPAD1<yExU^cE<Nhi zH1R|t9K9L&9DadKJbCl%2XL0*t<W%&d7l5y{N|b2XJ)>I|8|WeL!qlaUPAvC*Y;X` zzCYf_aS3q|=gofgWyG!+*XVV$pcP@Xp&cFQGy?Pny68=G(`MXa<b$*qV1D%MO9nwB zdeMh|3}DcZO@g6a*&wt9w`nWxVEBZ-yDwp6S276=V-yiij^f^-9Xqu5O=8Hj4Qu1H z9SZG0Bkkm^3sKsQ33|G0%v+ZB@UdP@(mqVlemuZKOk)O*FpD`n#yp<jDHaenLUaHL z&JRM$`Xb4LG?^yEt$vc`tG}eL#63X6d;@7Z*!RatWvA0(FzvZ$G&VV-Oz;|;o1U7P zjZW;0fvs;Fno}wj)^$rM>qS<um5<D_%~H`;-X4jx*S|Ad7k11|W+^$(3haE$iU?{m z$1#hBtz?gcpd)pIZZI8uZRMr#m!THh+Ge(+WP}&!;F+kE(;u_93QG2kDwx<A<_zNK zj+uXN78u{@p?>aZF=f2&6o+k=3%bK6<c^iVMZzlQ)0@}x8%$aBtl;YBz&4Gd&QBvN z{AFmNWEecRiYk2VW-GOa>)i#5<sF-6>`2w}wRqVo@nm?x)5l8S!IR4r8FB-fhEiVB zT&ZNca#?pa+yD<=)Hj*?^9mzzw|Oz8rZVreWM<`cDx<AtR2qJkO0K@paw{upGLg~J ziRF}<b(@x!bE(X7Le-Yi+3!9p!k@4esTP9?^11pW81C>(<YM*vtG|QEKDj}bFAj<x Gzx*E@(^p3T delta 327 zcmX|6%Syvg6wJA~O>0Q2wrTo)SX*O!ztBw=ZGXYt5qDEUq=K~QR&nW00<H?$r3+VC z`w#wuAm~2`-WKe@40AZc9L`VS*>-!$q^T)<HM-}&DNXyF4*#%}=_rnxkM8IeY2V@$ zGO&?_gB<cG_;FrDk(W^7Wt97LAztEr1C@#J2{BYrLmew<_%a}@j-^kWMw2sWVQrpv zCj#qZ5fIz(u%VJ3HfOpu)9rwK$tPLtu!CLBA;x*d0`|CweJ;(54yI?htacR~auwoK z^Nyk$Iws=qO|(r;jDpLnLH{}kzr_=Abf>p<GNX5+L&B>?XXZUEylTUidV1IjD^}MT bC@V!mFH=fbWExVJ!ia?*mLm)qK3Kg!S{O<1 diff --git a/devops/__pycache__/urls.cpython-37.pyc b/devops/__pycache__/urls.cpython-37.pyc index 2e9dc973885dd64f91ebefc212396854c2af0c0b..ba6c255a851c0682264c811be1b2c16d612937ee 100644 GIT binary patch delta 515 zcmX|;%}T>S5XZA=lIF8tYSp%gw~|Y9@+cJb?8So+kdV4VO=)Y%rmf`CLoZ%DNM1n@ zkG_BxPrgE~`Ubv$vsui*F#q5FXNH0O(Z9_8nd6ue#_#9(&9y2?-yHs<hGK}xKA!vT z#@8sJ>a;r3eVs_4Pz_YjCdQU*Wuz*|6BCJR9rU6api+}s)P9l+c93hdUUCDuNn0iF zAn($>lH17p^q}A_UwnuU9oiKQW5c(oawWOF_m@m=U%Dh$qdA3#)C>X&qvbdVylhxG z-sBpCcm*+b81rxjY!Ti<YK4o)6NZ=8g%~X3DGSi>P4U2WsxA?{)ZnG)$Q3SAgKrR? zQ+ma;&as%SuAwtx?9NNAHE<yoXacgwO3%p+gaoE@NL)L6t(;usj<`R$zn_l6*tc;T z-alAHux2s-lY|%GrMbeQ1ab@YB1{s9=j=_2Tj<BFtf`zHbx<nACJmw!yHb;FS(h86 HAyWPWAgq%f delta 293 zcmX@f^@Ky+iI<m)0SJ6m9AkemGcY^`abSQG$Z!DS;);ptnjyLDQS6KisVph1sZ3cM zS)9#GKsH+{JD6rqWliB|W(2c2fov`)n;XdHfwFmlY(5Y>oh^zhg+G`<Q()t($xPOo zjJH@b^HNePs@M`!ax?SvZ!uJ{7pE4Lr55Q|aYGn-rA0Z#Rjj2zo_-Z4hysgD)?*Qy z?873%?5D{+xs=7!G>QkRzAQ7fyts%J==xhM#hE3kw^%?X+~Nk31&JjksYQ9kD;bJ- ffbvB=li#t}iU|RkEQ~yi0*pM&Jd7Mb76<|W6<|s+ diff --git a/devops/celery.py b/devops/celery.py new file mode 100644 index 0000000..2537958 --- /dev/null +++ b/devops/celery.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import, unicode_literals +from celery import Celery +import os + +# 为celery程序设置默认的Django设置模块。 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", 'devops.settings') + +# 注册Celery的APP +app = Celery('devops') + +# 绑定配置文件 +app.config_from_object('django.conf.settings', namespace='CELERY') + +# 自动发现各个app下的tasks.py文件 +app.autodiscover_tasks() + diff --git a/devops/settings.py b/devops/settings.py index 7590621..ee9702d 100644 --- a/devops/settings.py +++ b/devops/settings.py @@ -33,7 +33,7 @@ # Application definition INSTALLED_APPS = [ - 'simpleui', + # 'simpleui', # 不兼容 debug_toolbar 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -45,9 +45,11 @@ 'user', 'webssh', 'webtelnet', + # 'debug_toolbar', ] MIDDLEWARE = [ + # 'debug_toolbar.middleware.DebugToolbarMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -134,3 +136,28 @@ # session 如果在此期间未做任何操作,则退出, django 本身要么设置固定时间,要么关闭浏览器失效 CUSTOM_SESSION_EXIPRY_TIME = 60 * 30 # 30 分钟 + +# celery 配置 +CELERY_BROKER_URL = 'redis://127.0.0.1:6379/0' + + +DEBUG_TOOLBAR_PANELS = [ + 'debug_toolbar.panels.versions.VersionsPanel', + 'debug_toolbar.panels.timer.TimerPanel', + 'debug_toolbar.panels.settings.SettingsPanel', + 'debug_toolbar.panels.headers.HeadersPanel', + 'debug_toolbar.panels.request.RequestPanel', + 'debug_toolbar.panels.sql.SQLPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', + 'debug_toolbar.panels.templates.TemplatesPanel', + 'debug_toolbar.panels.cache.CachePanel', + 'debug_toolbar.panels.signals.SignalsPanel', + 'debug_toolbar.panels.logging.LoggingPanel', + 'debug_toolbar.panels.redirects.RedirectsPanel', + 'debug_toolbar.panels.profiling.ProfilingPanel', +] +INTERNAL_IPS = [ + # ... + '127.0.0.1', + # ... +] diff --git a/devops/urls.py b/devops/urls.py index b0b9e52..5b87d57 100644 --- a/devops/urls.py +++ b/devops/urls.py @@ -15,13 +15,24 @@ """ from django.contrib import admin from django.urls import path, include +import debug_toolbar from server.views import index urlpatterns = [ + path('__debug__/', include(debug_toolbar.urls)), + path('admin/', admin.site.urls), + path('', index), - path('server/', include('server.urls')), - path('user/', include('user.urls')), - path('webssh/', include('webssh.urls')), - path('webtelnet/', include('webtelnet.urls')), + + path('server/', include('server.urls', namespace='server')), + path('api/server/', include('server.urls_api', namespace='server_api')), + + path('user/', include('user.urls', namespace='user')), + path('api/user/', include('user.urls_api', namespace='user_api')), + + path('webssh/', include('webssh.urls', namespace='webssh')), + + path('webtelnet/', include('webtelnet.urls', namespace='webtelnet')), + ] diff --git a/requirements.txt b/requirements.txt index 925230a..cff46bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ django==2.2.3 +supervisor +simpleui paramiko channels +celery==4.3.0 +redis +eventlet +django-debug-toolbar diff --git a/run.bat b/run.bat index 78b1cff..1d9573b 100644 --- a/run.bat +++ b/run.bat @@ -1 +1,2 @@ python3 manage.py runserver +pause diff --git a/run_celery_work.bat b/run_celery_work.bat new file mode 100644 index 0000000..7f2b896 --- /dev/null +++ b/run_celery_work.bat @@ -0,0 +1,2 @@ +"C:\Program Files\Python37\Scripts\celery.exe" -A devops worker -l info +pause diff --git a/server/__pycache__/tasks.cpython-37.pyc b/server/__pycache__/tasks.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2431b644f9d5452511a9ed2aad46d2c5cd8d69a9 GIT binary patch literal 394 zcmYLEu}T9$5Z&3mJI<JE?F8Gi2!e$mA`-C@(%2m*A>1w)-ODYzn?S6D*!dG$iT~i2 zY%3|!T3R`CXK-L<-ptOtH#^L-9z)x|9^QUq{-R=Q0>K5kJHjBD1kGwF00?u<Yf*?6 z3mp}a5;BtUGh0CsOL4>UWP@8Qhae`#V2b?2J&Y=9ZjA&k(cK;fj{++s_wdRke<K;9 ziAM7TU)P^c>-U!p9s9WJmGcLc(#kG#5n^MfGmre!T7}qYrOXGjDNeTG=kV&ZG)sTi z%#KgS#y0n=@}*RZ#<<ccyHK{I)7-!;LrXpGsCSUTkv8Kg^uUsKo)lTPI_QQh$8Jde g3i^<xm3H|5h7J*7NYCm<&b2xRtXld$^dU`t0RdxJRR910 literal 0 HcmV?d00001 diff --git a/server/__pycache__/urls.cpython-37.pyc b/server/__pycache__/urls.cpython-37.pyc index 77cae2ad725f93058f9556748c14e6891813b037..9f7f4cf34b50489c82f127e20e7c5f1860283e3c 100644 GIT binary patch delta 68 zcmaFK^puI$iI<m)0SK<1a*lm5k#{jyMt*TgvHmUAjQokaQbaUetzrr)OEU8FjLqW< Xit@8klS?L>G3u~0u`uy5@-YGc5;zqa delta 72 zcmaFL^pc6!iI<m)0SLNUZDZd|<Xy~_lUZC+tbdC&Cv)Pi6bW5ttC-N@)S}{;oYb_m a;^G*W)Z*-t{DR3wj5_ShEKGcie2f5Ez7|XX diff --git a/server/__pycache__/urls_api.cpython-37.pyc b/server/__pycache__/urls_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dadd6139e70683d12849c6cad0d649638de25aa0 GIT binary patch literal 294 zcmXv|!A`?43{9G}>sa?8J+Mh|Ktc#15LYg6s*0*;wzgGm6D3*E_y<mW1;4>(<jQHk zz-bN>mh9(ey=VJ=K0jj=FW(QZpM*b={6|X79i_XV0t|S^dR}qPAOX=Oy{H8BvXaOl zMbV~^!Q?}%d6hx_%*yG3e5~X_8s#?!ej?Lxi*K$`W*ahxc0~K0dUSpnWP~UmRF6;P zB+Q^y!&)zPwsSTidtg>Z$dxfoRM{-j842iwM?1JLW(%7AmEZHn>)P!7Mh}-)jj_7L jm9GIuZCri2UB}(=nT6Sh{HE8i>+m+d>f-8x7l}-c9Vbnp literal 0 HcmV?d00001 diff --git a/server/__pycache__/views.cpython-37.pyc b/server/__pycache__/views.cpython-37.pyc index e258ba0aa80a3c4c994d1c983a9713f200b03ff8..b846b894993fbc358e277e7f2a974ad2619d907d 100644 GIT binary patch delta 667 zcmZuv&2G~`5Z+n;$Bvs&ff9)`$Px;-N>m7>_KK>a-YivB$u5Z_$F6qQ98ggX;o3tK z9-zH);~}{8&<F4{^bLApW}ziQ>}o!J^FQPDZSXxx_qyE<!}Ifc|9IP7r2{al=Wllf z6Mh-blRN<?9TAA|D_cU|MIMQc<*9U}D`Jt5hQ2M)Jz`n9`Zn=0XyN_p?gsknYuH&t zSYL<Kg{MGQZ6hB8S@3WaA7vj!A0nKy851yM>WP6FzjUtH7pPuX?i$Z}`H%_c0yZ@F z0`}MyT>2Dkf^#N36E4}(Ia;vHKgi=k>*h?VMoZO(gw^R>W{{`EPfj*0k0+{b&VHNz zIzIO??vqq6r?obEG&S=wi`qa-t&6%+w1J8V5`@<OSd>O8g&+4FYeP|JQ&eN4Xjzpa zWL#}gpl8deKG`iiA~rxEP20!e9Z0wj&$t?_fACj}`$=f#Ka+SWpuC3x{?o`t|J!JY zSx>2vi@=-?D6$-6y}T!8r`4n$>1nOZxG~z6%%jGvD`)q(z4wa`qj@c4scBbcpVp3V f3vIM}Sgs9UV?tBPH}nVS9zp^=dlNSalbFLVxxbWB delta 418 zcmaKn%}T^D5XU=hx6QUmsO!4;2GUEt34*vNUiIQ_MMO;4DqRbc)~g47fzlW7?8Eo~ z`U*aTGZpdUV21oNnas>@@;rWz^`TOUM=L)*I2S>uH=*pd=7kMRWa3Bf0TwCw!i)!B z+Q5b;F)82ZO&NLW<*IvH_7Ywo>)&8!T~Is6;`tibaMGUzyLRR*-dWr(Yu8*<*RF9z z*{rI3gc0Q;kAW$R&!R|jA2l7XBQ9~6aeR=qAv*Nny9-G?KLMOF>Hdq<67e@Fro4Yi zT=JKM8&ox;{mBW4!;zC*FEVp;dAqC&SJv2EwKOCR-|KLGgcC+hSvdK}@ZUcWd#l>C TmE8x*aQsyX8CxiYjC}Y2`AAX5 diff --git a/server/__pycache__/views_api.cpython-37.pyc b/server/__pycache__/views_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..85c21aee3e190ced11783aaeceb6081464a59b13 GIT binary patch literal 454 zcmYjNy-ve05VrFV{Sh0Cbj*?kBm_bTQ6VvPfGrA&qS$IvoY>e-Q4mb!L15%Hcnq&h zeFY}Y0VK}4@9wPo?mOMZXta&Mjz6ZiyBMKQTl|>|$vL<>1R#iDhH9K*Y;%V>wVS$- zyToH&?WaCQ)FnO%UeFq+0SRwV9BqJ!Vr+(2T8nF{1aB0Ddd&E|YH~^M+e%Wh0cqPd z&{XNXq>Re0w13u|U!Ou$djJ}};y3h+wey76u#R>KM38f4k7GMw|7XHvGKoFu1H<@) zvZ6D-kX56loftbNwDQny0!dZN^gELF7+=ZdNhUhI;LX8NE+k*lQfGv&xKNp*az$mf zs_28t3sEJaGoIB9fvExj<U}~aatPTh4$Tf(7R{U|YQd#0TdjI^v(@T~C7N?4?U{|Q f3w5XZWqWGB$v+s_rs%Zhq-FHXo~9QYVtC#+%S>%F literal 0 HcmV?d00001 diff --git a/server/tasks.py b/server/tasks.py new file mode 100644 index 0000000..389f7a0 --- /dev/null +++ b/server/tasks.py @@ -0,0 +1,14 @@ +from devops.celery import app +from celery import task +import time +# Create your tests here. + + +# @app.task(ignore_result=True) +# @app.task +@task +def test_celery(): + print("开始") + time.sleep(10) + return "test celery" + diff --git a/server/urls.py b/server/urls.py index 9d6817e..ee66328 100644 --- a/server/urls.py +++ b/server/urls.py @@ -1,11 +1,10 @@ from django.urls import path from . import views - -app_name = 'server' +app_name="server" urlpatterns = [ path('', views.index, name='index'), - path('lists/', views.lists, name='lists'), + path('hosts/', views.hosts, name='hosts'), path('users/', views.users, name='users'), ] diff --git a/server/urls_api.py b/server/urls_api.py new file mode 100644 index 0000000..ac849bd --- /dev/null +++ b/server/urls_api.py @@ -0,0 +1,8 @@ +from django.urls import path +from . import views_api + +app_name="server" +urlpatterns = [ + path('test/', views_api.test, name='test'), +] + diff --git a/server/views.py b/server/views.py index c014cfb..c8c3695 100644 --- a/server/views.py +++ b/server/views.py @@ -2,6 +2,7 @@ from util.tool import login_required, admin_required from .models import RemoteUserBindHost, RemoteUser from user.models import User, Group +from django.db.models import Q # Create your views here. @@ -14,13 +15,19 @@ def index(request): @login_required -def lists(request): - hosts = RemoteUserBindHost.objects.all() - return render(request, 'server/host_lists.html', locals()) +def hosts(request): + if request.session['issuperuser']: + hosts = RemoteUserBindHost.objects.all() + else: + hosts = RemoteUserBindHost.objects.filter( + Q(user__username = request.session['username']) | Q(group__user__username = request.session['username']) + ).distinct() + return render(request, 'server/hosts.html', locals()) + - @login_required @admin_required def users(request): users = RemoteUser.objects.all() - return render(request, 'server/user_lists.html', locals()) + return render(request, 'server/users.html', locals()) + diff --git a/server/views_api.py b/server/views_api.py new file mode 100644 index 0000000..1970450 --- /dev/null +++ b/server/views_api.py @@ -0,0 +1,12 @@ +from django.shortcuts import HttpResponse +from util.tool import login_required +from .tasks import test_celery +# Create your views here. + + +@login_required +def test(request): + result = test_celery.delay() + print(result) + return HttpResponse('test celery!!!') + diff --git a/start_docker.sh b/start_docker.sh new file mode 100644 index 0000000..828fb1f --- /dev/null +++ b/start_docker.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +docker rm -f devops +docker rmi -f devops + +docker build -t devops . +docker run -d --name devops -p 8000:8000 devops diff --git a/templates/base.html b/templates/base.html index 18afa3d..f2c2827 100644 --- a/templates/base.html +++ b/templates/base.html @@ -72,7 +72,7 @@ <img src="{% static 'adminlte/dist/img/timg.jpg' %}" class="img-circle elevation-1" style="max-width:100%;height:100%;"> <small>{{ request.session.nickname }}</small> </a> <div class="dropdown-menu" x-placement="bottom-start" style="position: absolute; will-change: transform; top: 0px; left: 0px; transform: translate3d(0px, 40px, 0px);"> - <a class="dropdown-item personinfo" tabindex="-1" href="javascript:void(0);" onclick="getuserinfo();"><small>个人信息</small></a> + <a class="dropdown-item personinfo" tabindex="-1" href="{% url 'user:profile' %}" ><small>个人信息</small></a> <a class="dropdown-item changepasswd" tabindex="-1" href="javascript:void(0);"><small>修改密码</small></a> </div> </li> @@ -92,7 +92,7 @@ <div class="container-fluid"> <div class="row"> <div class="col-12 p-3"> - <h5 class="text-center mt-3">确定要退出吗?</h5> + <h4 class="text-center mt-3">确定要退出吗?</h4> </div> <div class="col-6 p-3"> <button type="button" class="btn btn-block btn-secondary btn-flat" data-iziModal-close>取消</button> @@ -109,7 +109,10 @@ <h5 class="text-center mt-3">确定要退出吗?</h5> <form role="form" method="POST" onsubmit="changepasswd(this);return false;"> {% csrf_token %} <div class="card-body"> - <h6 class="mb-3"><strong>修改密码</strong></h6> + <div class="row"> + <div class="col-8"><h4 class="mb-3"><strong>修改密码</strong></h4></div> + <div class="col-4"><a href="javascript:void(0)" class="iziModal-button-close float-right" data-izimodal-close="" style="color:black;vertical-align:middle"><i class="fas fa-times fa-lg"></i></a></div> + </div> <div class="form-group"> <label><small>当前密码</small></label> <input type="password" class="form-control" name="oldpasswd" placeholder="Password" maxlength="256" required> @@ -166,7 +169,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> <a href="{% url 'server:index' %}" class="nav-link"> <i class="nav-icon fas fa-tachometer-alt fa-xs"></i> <p> - 仪表盘 - x + 仪表盘 </p> </a> </li> @@ -181,7 +184,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> </a> <ul class="nav nav-treeview"> <li class="nav-item"> - <a href="{% url 'user:lists' %}" class="nav-link"> + <a href="{% url 'user:users' %}" class="nav-link"> <!--i class="fas fa-user-edit nav-icon"></i--> <p> 用户</p> </a> @@ -204,7 +207,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> </a> <ul class="nav nav-treeview"> <li class="nav-item"> - <a href="{% url 'server:lists' %}" class="nav-link"> + <a href="{% url 'server:hosts' %}" class="nav-link"> <p> 主机列表</p> </a> </li> @@ -227,12 +230,12 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> </a> <ul class="nav nav-treeview"> <li class="nav-item"> - <a href="{% url 'server:lists' %}" class="nav-link"> + <a href="{% url 'server:hosts' %}" class="nav-link"> <p> 批量命令 - x</p> </a> </li> <li class="nav-item"> - <a href="{% url 'webssh:lists' %}" class="nav-link"> + <a href="{% url 'webssh:hosts' %}" class="nav-link"> <p> web终端</p> </a> </li> @@ -254,7 +257,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> </a> </li> <li class="nav-item"> - <a href="{% url 'server:lists' %}" class="nav-link"> + <a href="{% url 'server:hosts' %}" class="nav-link"> <p> 批量命令日志 - x</p> </a> </li> @@ -370,7 +373,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> //subtitle: "确认退出", iconClass: 'icon-announcement', width: 450, - padding: 10, + //padding: 10, }); $(document).on('click', '.logout', function (event) { event.preventDefault(); @@ -381,7 +384,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> $("#modal-changepasswd").iziModal({ iconClass: 'icon-announcement', width: 650, - padding: 5, + //padding: 5, overlayClose: false, // 是否允许点击模态窗口的外部来关闭模态窗口。 closeOnEscape: false, // 是否允许通过点击ESC键来关闭模态窗口。 }); @@ -406,7 +409,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> csrfmiddlewaretoken = '{{ request.COOKIES.csrftoken }}'; $.ajax({ - url: "{% url 'user:changepasswd' %}", + url: "{% url 'user_api:password_update' %}", async: true, type: 'POST', dataType: 'json', @@ -450,98 +453,7 @@ <h6 class="mb-3"><strong>修改密码</strong></h6> $(thisObj.find("input[name='newpasswdagain']")[0]).val(''); return false; } - - - getuserinfo = function () { - $("#modal-userinfo-container").html(''); - $.ajax({ - url: "{% url 'user:userinfo' %}", - async: false, // false 才能保证重新获取的验证码有效 - type: 'GET', - dataType: 'json', - timeout: 10000, - cache: true, - beforeSend: LoadFunction, //加载执行方法 - error: errFunction, //错误执行方法 - success: succFunction //成功执行方法 - }); - - function LoadFunction() { - $("#modal-userinfo-container").html('<div id="modal-userinfo" class="iziModal"><div class="row"><h4 class="m-auto">获取中...</h4></div></div>'); - }; - function errFunction() { - $("#modal-userinfo-container").html('<div id="modal-userinfo" class="iziModal"><div class="row"><h4 class="m-auto">获取错误</h4></div></div>'); - }; - - function succFunction(res) { - if (res.code != 200) { - $("#modal-userinfo-container").html('<div id="modal-userinfo" class="iziModal"><div class="row"><h4 class="m-auto">获取错误</h4></div></div>'); - } else { - var userid = res.user.id - var username = res.user.username - var nickname = res.user.nickname - var email = res.user.email - var sex = res.user.sex - if (sex == 'male') { - sex = '男'; - } else if (sex == 'female') { - sex = '女'; - } else { - sex = '其他'; - } - - var enabled = res.user.enabled - if (enabled == true) { - enabled = '启用'; - } else if (enabled == false) { - enabled = '禁用'; - } else { - enabled = '其他'; - } - var create_time = res.user.create_time - - var groups = res.user.groups - if (groups == null) { - groups = 'N/A' - } - - var role = res.user.role - if (role == 0) { - role = '其他'; - } else if (role == 1) { - role = '超级管理员'; - } else if (role == 2){ - role = '普通用户'; - } - $("#modal-userinfo-container").html('<div id="modal-userinfo" class="iziModal"><div class="row p-4">\ - <div class="col-12 mb-2"><h6><strong>个人信息</strong></h6></div>\ - <div class="col-4 mb-1 border-bottom">用户ID: </div><div class="col-8 mb-1 border-bottom">' + userid + '</div>\ - <div class="col-4 mb-1 border-bottom">用户名: </div><div class="col-8 mb-1 border-bottom">' + username + '</div>\ - <div class="col-4 mb-1 border-bottom">用户昵称: </div><div class="col-8 mb-1 border-bottom">' + nickname + '</div>\ - <div class="col-4 mb-1 border-bottom">用户邮箱: </div><div class="col-8 mb-1 border-bottom">' + email + '</div>\ - <div class="col-4 mb-1 border-bottom">用户性别: </div><div class="col-8 mb-1 border-bottom">' + sex + '</div>\ - <div class="col-4 mb-1 border-bottom">用户状态: </div><div class="col-8 mb-1 border-bottom">' + enabled + '</div>\ - <div class="col-4 mb-1 border-bottom">用户创建时间: </div><div class="col-8 mb-1 border-bottom">' + create_time + '</div>\ - <div class="col-4 mb-1 border-bottom">用户组: </div><div class="col-8 mb-1 border-bottom">' + groups + '</div>\ - <div class="col-4 mb-1 border-bottom">用户角色: </div><div class="col-8 mb-1 border-bottom">' + role + '</div>\ - <div class="col-12 mt-3"><button type="button" class="btn btn-block btn-success btn-flat" data-iziModal-close>返回</button></div>\ - </div></div>'); - } - }; - - // 初始化弹出框 - $("#modal-userinfo").iziModal({ - iconClass: 'icon-announcement', - width: 650, - padding: 5, - overlayClose: true, // 是否允许点击模态窗口的外部来关闭模态窗口。 - closeOnEscape: true, // 是否允许通过点击ESC键来关闭模态窗口。 - }); - // 打开弹出框 - $('#modal-userinfo').iziModal('open'); - } - </script> {% block js %} diff --git a/templates/server/hosts.html b/templates/server/hosts.html new file mode 100644 index 0000000..42fe78f --- /dev/null +++ b/templates/server/hosts.html @@ -0,0 +1,144 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>主机列表</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">主机管理</li> + <li class="breadcrumb-item active">主机列表</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title"> + 主机列表{% if request.session.issuperuser %}<a href="javascript:void(0)" class="btn btn-sm btn-success ml-3 addsoft">新增 + </a>{% endif %} + </h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-hover"> + <thead> + <tr> + <th>主机ID</th> + <th>类型</th> + <th>环境</th> + <th>名称</th> + <th>IP</th> + <th>公网IP</th> + <th>协议</th> + <th>端口</th> + <th>系统</th> + <th>用户名</th> + <th>添加时间</th> + </tr> + </thead> + <tbody> + {% for host in hosts %} + <tr> + <td>{{ host.id }}</td> + <td>{{ host.host.get_type_display }}</td> + <td>{{ host.host.get_env_display }}</td> + {% if host.remote_user %} + <td>{{ host.host.hostname }}_{{ host.remote_user.name }}</td> + {% else %} + <td>{{ host.host.hostname }}</td> + {% endif %} + <td>{{ host.host.ip }}</td> + <td>{{ host.host.wip | default:'' }}</td> + <td>{{ host.host.get_protocol_display }}</td> + <td>{{ host.host.port }}</td> + <td>{{ host.host.release }}</td> + {% if host.remote_user %} + <td>{{ host.remote_user.username }}</td> + {% else %} + <td>N/A</td> + {% endif %} + <td>{{ host.create_time|date:"Y/m/d H:i:s" }}</td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>主机ID</th> + <th>类型</th> + <th>环境</th> + <th>名称</th> + <th>IP</th> + <th>公网IP</th> + <th>协议</th> + <th>端口</th> + <th>系统</th> + <th>用户名</th> + <th>添加时间</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/server/users.html b/templates/server/users.html new file mode 100644 index 0000000..422aeb3 --- /dev/null +++ b/templates/server/users.html @@ -0,0 +1,118 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户列表</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">主机管理</li> + <li class="breadcrumb-item active">用户列表</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title"> + 用户列表<a href="javascript:void(0)" class="btn btn-sm btn-success ml-3 addsoft">新增 + </a> + </h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-hover"> + <thead> + <tr> + <th>ID</th> + <th>名称</th> + <th>用户名</th> + <th>备注</th> + <th>添加时间</th> + </tr> + </thead> + <tbody> + {% for user in users %} + <tr> + <td>{{ user.id }}</td> + <td>{{ user.name }}</td> + <td>{{ user.username }}</td> + <td>{{ user.memo | default:'' }}</td> + <td>{{ user.create_time|date:"Y/m/d H:i:s" }}</td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>ID</th> + <th>名称</th> + <th>用户名</th> + <th>备注</th> + <th>添加时间</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/groups.html b/templates/user/groups.html new file mode 100644 index 0000000..f657c8e --- /dev/null +++ b/templates/user/groups.html @@ -0,0 +1,113 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户组</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">用户管理</li> + <li class="breadcrumb-item active">用户组</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">用户组<a href="javascript:void(0)" class="btn btn-sm btn-success ml-3 addsoft">新增 + </a></h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-hover"> + <thead> + <tr> + <th>组ID</th> + <th>组名</th> + <th>备注</th> + <th>创建时间</th> + </tr> + </thead> + <tbody> + {% for group in groups %} + <tr> + <td>{{ group.id }}</td> + <td>{{ group.group_name }}</td> + <td>{{ group.memo | default:'' }}</td> + <td>{{ group.create_time|date:"Y/m/d H:i:s" }}</td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>组ID</th> + <th>组名</th> + <th>备注</th> + <th>创建时间</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/logs.html b/templates/user/logs.html new file mode 100644 index 0000000..c81b54f --- /dev/null +++ b/templates/user/logs.html @@ -0,0 +1,119 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户日志</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">日志审计</li> + <li class="breadcrumb-item active">用户日志</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">用户日志</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-striped"> + <thead> + <tr> + <th>操作人</th> + <th>类型</th> + <th>详情</th> + <th>IP地址</th> + <th>User_Agent</th> + <th>事件时间</th> + </tr> + </thead> + <tbody> + {% for log in logs %} + <tr> + <td>{{ log.user | default:'' }}</td> + <td>{{ log.get_event_type_display }}</td> + <td>{{ log.detail }}</td> + <td>{{ log.address }}</td> + <td>{{ log.useragent | truncatechars_html:25 }}...</td> + <td>{{ log.create_time|date:"Y/m/d H:i:s" }}</td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>操作人</th> + <th>类型</th> + <th>详情</th> + <th>IP地址</th> + <th>User_Agent</th> + <th>事件时间</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/profile.html b/templates/user/profile.html new file mode 100644 index 0000000..0c9f2a6 --- /dev/null +++ b/templates/user/profile.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>个人信息</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active">个人信息</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">个人信息</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <a class="btn btn-tool" href="{% url 'user:profile_edit' %}" title="修改"> + <i class="fas fa-wrench"></i> + </a> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body row"> + <div class="col-3 pt-1 pb-1 border-bottom">用户名:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.username }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">昵称:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.nickname }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">邮箱:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.email }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">手机:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.phone | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">微信:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.weixin | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">QQ:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.qq | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">性别:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.get_sex_display }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">是否启用:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.enabled }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">角色:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.get_role_display }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">所属组:</div><div class="col-9 pt-1 pb-1 border-bottom">{% if user.groups.all %}{% for group in user.groups.all %}<button type="button" class="btn btn-sm btn-success btn-flat">{{ group.group_name }}</button> {% endfor %}{% else %}{% endif %}</div> + <div class="col-3 pt-1 pb-1 border-bottom">创建时间:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.create_time | date:"Y/m/d H:i:s" }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">最后登录时间:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.last_login_time | date:"Y/m/d H:i:s" | default:'' }}</div> + <div class="col-3 pt-1 pb-1">备注:</div><div class="col-9 pt-1 pb-1">{{ user.memo | default:'' }}</div> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/profile_edit.html b/templates/user/profile_edit.html new file mode 100644 index 0000000..c3429bc --- /dev/null +++ b/templates/user/profile_edit.html @@ -0,0 +1,130 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>个人信息设置</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active">个人信息设置</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">个人信息</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body row"> + <div class="col-2 pt-1 pb-1">用户名:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" value="{{ user.username }}" disabled></div> + <div class="col-2 pt-1 pb-1">昵称:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="nickname" value="{{ user.nickname }}"></div> + <div class="col-2 pt-1 pb-1">邮箱:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="email" value="{{ user.email }}"></div> + <div class="col-2 pt-1 pb-1">手机:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="phone" value="{{ user.phone | default:'' }}"></div> + <div class="col-2 pt-1 pb-1">微信:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="weixin"value="{{ user.weixin | default:'' }}"></div> + <div class="col-2 pt-1 pb-1">QQ:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="qq" value="{{ user.qq | default:'' }}"></div> + <div class="col-2 pt-1 pb-1">性别:</div> + <div class="col-10 pt-1 pb-1"> + <select class="form-control select2" id="sex" style="width: 100%;"> + <option {% if user.get_sex_display == '男' %}selected="selected"{% endif %} value="male">男</option> + <option {% if user.get_sex_display == '女' %}selected="selected"{% endif %} value="female">女</option> + </select> + </div> + <div class="col-2 pt-1 pb-1">备注:</div><div class="col-10 pt-1 pb-1"><textarea id="memo" class="form-control">{{ user.memo | default:'' }}</textarea></div> + <div class="offset-2 col-10 pt-2"><button class="btn btn-default" type="reset">重置</button><button class="btn btn-success ml-2" onclick="changeuserprofile(this);">提交</button></div> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} + +<script> +// 修改个人信息 +changeuserprofile = function(event) { + toastr.options.closeButton = true; + toastr.options.showMethod = 'slideDown'; + toastr.options.hideMethod = 'fadeOut'; + toastr.options.closeMethod = 'fadeOut'; + toastr.options.timeOut = 3000; + toastr.options.extendedTimeOut = 0; + + $(event).removeAttr("onclick"); + $(event).attr("disabled", true); + + var nickname = $('#nickname').val(); + var email = $('#email').val(); + var phone = $('#phone').val(); + var weixin = $('#weixin').val(); + var qq = $('#qq').val(); + //var sex = $("#sex").find("option:selected").text(); + var sex = $("#sex").find("option:selected").val(); + var memo = $('#memo').val(); + csrfmiddlewaretoken = '{{ request.COOKIES.csrftoken }}'; + + $.ajax({ + url: "{% url 'user_api:profile_update' %}", + async: true, + type: 'POST', + dataType: 'json', + data: { + 'csrfmiddlewaretoken': csrfmiddlewaretoken, + 'nickname': nickname, + 'email': email, + 'phone': phone, + 'weixin': weixin, + 'qq': qq, + 'sex': sex, + 'memo': memo, + }, + timeout: 5000, + cache: true, + beforeSend: LoadFunction, //加载执行方法 + error: errFunction, //错误执行方法 + success: succFunction, //成功执行方法 + }); + + function LoadFunction() { + // 提交中 + }; + + function errFunction() { + // 消息框 + toastr.error('更新个人信息错误'); + $(event).removeAttr("disabled"); + $(event).attr("onclick", "changeuserprofile(this);"); + }; + + function succFunction(res) { + if (res.code != 200) { + // 消息框 + toastr.error('更新个人信息错误: ' + res.err); + $(event).removeAttr("disabled"); + $(event).attr("onclick", "changeuserprofile(this);"); + } else { + // 消息框 + toastr.success('更新个人信息成功'); + } + }; +} + +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/user.html b/templates/user/user.html new file mode 100644 index 0000000..ab918d1 --- /dev/null +++ b/templates/user/user.html @@ -0,0 +1,102 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户信息</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">用户管理</li> + <li class="breadcrumb-item"><a href="{% url 'user:users' %}">用户</a></li> + <li class="breadcrumb-item active">用户信息</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">用户信息</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <a class="btn btn-tool" href="{% url 'user:user_edit' user.id %}" title="修改"> + <i class="fas fa-wrench"></i> + </a> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body row"> + <div class="col-3 pt-1 pb-1 border-bottom">用户名:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.username }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">昵称:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.nickname }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">邮箱:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.email }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">手机:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.phone | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">微信:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.weixin | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">QQ:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.qq | default:'' }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">性别:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.get_sex_display }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">是否启用:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.enabled }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">角色:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.get_role_display }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">所属组:</div><div class="col-9 pt-1 pb-1 border-bottom">{% if user.groups.all %}{% for group in user.groups.all %}<button type="button" class="btn btn-sm btn-success btn-flat">{{ group.group_name }}</button> {% endfor %}{% else %}{% endif %}</div> + <div class="col-3 pt-1 pb-1 border-bottom">创建时间:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.create_time | date:"Y/m/d H:i:s" }}</div> + <div class="col-3 pt-1 pb-1 border-bottom">最后登录时间:</div><div class="col-9 pt-1 pb-1 border-bottom">{{ user.last_login_time | date:"Y/m/d H:i:s" | default:'' }}</div> + <div class="col-3 pt-1 pb-1">备注:</div><div class="col-9 pt-1 pb-1">{{ user.memo | default:'' }}</div> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/user/user_edit.html b/templates/user/user_edit.html new file mode 100644 index 0000000..3201d8c --- /dev/null +++ b/templates/user/user_edit.html @@ -0,0 +1,218 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户信息设置</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item active">用户信息设置</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">用户信息</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body row"> + <div class="col-2 pt-1 pb-1">用户名:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="username" value="{{ user.username }}" disabled></div> + <div class="col-2 pt-1 pb-1">昵称:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="nickname" value="{{ user.nickname }}"></div> + <div class="col-2 pt-1 pb-1">邮箱:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="email" value="{{ user.email }}"></div> + <div class="col-2 pt-1 pb-1">手机:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="phone" value="{{ user.phone | default:'' }}"></div> + <div class="col-2 pt-1 pb-1">微信:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="weixin"value="{{ user.weixin | default:'' }}"></div> + <div class="col-2 pt-1 pb-1">QQ:</div><div class="col-10 pt-1 pb-1"><input class="form-control" type="text" id="qq" value="{{ user.qq | default:'' }}"></div> + + <div class="col-2 pt-1 pb-1">用户组:</div> + <div class="col-5 pt-1 pb-1"> + <div class="form-group"> + <span>所属组</span> + <select multiple class="form-control" id="group_select_left" style="min-height:150px"> + {% for group in user.groups.all %} + <option value="{{ group.id }}">{{ group.group_name }}</option> + {% endfor %} + </select> + </div> + </div> + <div class="col-5 pt-1 pb-1"> + <div class="form-group"> + <span>其他组</span> + <select multiple class="form-control" id="group_select_right" style="min-height:150px"> + {% for group in other_groups %} + <option value="{{ group.id }}">{{ group.group_name }}</option> + {% endfor %} + </select> + </div> + </div> + + <div class="col-2 pt-1 pb-1">性别:</div> + <div class="col-10 pt-1 pb-1"> + <select class="form-control" id="sex" style="width: 100%;"> + <option {% if user.get_sex_display == '男' %}selected="selected"{% endif %} value="male">男</option> + <option {% if user.get_sex_display == '女' %}selected="selected"{% endif %} value="female">女</option> + </select> + </div> + <div class="col-2 pt-1 pb-1">角色:</div> + <div class="col-10 pt-1 pb-1"> + <select class="form-control" id="role" style="width: 100%;"> + <option {% if user.get_role_display == '普通用户' %}selected="selected"{% endif %} value="2">普通用户</option> + <option {% if user.get_role_display == '超级管理员' %}selected="selected"{% endif %} value="1">超级管理员</option> + </select> + </div> + <div class="col-2 pt-1 pb-1">备注:</div><div class="col-10 pt-1 pb-1"><textarea id="memo" class="form-control">{{ user.memo | default:'' }}</textarea></div> + <div class="offset-2 col-10 pt-1 pb-1"> + <div class="custom-switch custom-switch-on-success"> + <input type="checkbox" class="custom-control-input" id="enabled" {% if user.enabled %}checked{% endif %}> + <label class="custom-control-label" for="enabled">启用</label> + </div> + </div> + <div class="offset-2 col-10 pt-2"><button class="btn btn-default" type="reset">重置</button><button class="btn btn-success ml-2" onclick="changeuserprofile(this);">提交</button></div> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} + +<script> +// 修改个人信息 +changeuserprofile = function(event) { + toastr.options.closeButton = true; + toastr.options.showMethod = 'slideDown'; + toastr.options.hideMethod = 'fadeOut'; + toastr.options.closeMethod = 'fadeOut'; + toastr.options.timeOut = 3000; + toastr.options.extendedTimeOut = 0; + + $(event).removeAttr("onclick"); + $(event).attr("disabled", true); + + var username = $('#username').val(); + var nickname = $('#nickname').val(); + var email = $('#email').val(); + var phone = $('#phone').val(); + var weixin = $('#weixin').val(); + var qq = $('#qq').val(); + //var sex = $("#sex").find("option:selected").text(); + var sex = $("#sex").find("option:selected").val(); + var role = $("#role").find("option:selected").val(); + var memo = $('#memo').val(); + var enabled; + if ($("#enabled").is(':checked')) { + enabled = true; + } else { + enabled = false; + } + + var groups = new Array(); + $("#group_select_left option").each(function(){ + var value = $(this).val(); //获取option值 + groups.push(value); + }); + groups = groups.join(",") + csrfmiddlewaretoken = '{{ request.COOKIES.csrftoken }}'; + + $.ajax({ + url: "{% url 'user_api:user_update' %}", + async: true, + type: 'POST', + dataType: 'json', + data: { + 'csrfmiddlewaretoken': csrfmiddlewaretoken, + 'username': username, + 'nickname': nickname, + 'email': email, + 'phone': phone, + 'weixin': weixin, + 'qq': qq, + 'sex': sex, + 'memo': memo, + 'enabled': enabled, + 'role': role, + 'groups': groups, + }, + timeout: 5000, + cache: true, + beforeSend: LoadFunction, //加载执行方法 + error: errFunction, //错误执行方法 + success: succFunction, //成功执行方法 + }); + + function LoadFunction() { + // 提交中 + }; + + function errFunction() { + // 消息框 + toastr.error('更新用户信息错误'); + $(event).removeAttr("disabled"); + $(event).attr("onclick", "changeuserprofile(this);"); + }; + + function succFunction(res) { + if (res.code != 200) { + // 消息框 + toastr.error('更新用户信息错误: ' + res.err); + $(event).removeAttr("disabled"); + $(event).attr("onclick", "changeuserprofile(this);"); + } else { + // 消息框 + toastr.success('更新用户信息成功'); + } + }; +} + +// 左右选项框 +$(function(){ + //移到右边 + $('#add').click(function() { + //获取选中的选项,删除并追加给对方 + $('#select1 option:selected').appendTo('#select2'); + }); + //移到左边 + $('#remove').click(function() { + $('#select2 option:selected').appendTo('#select1'); + }); + + //全部移到右边 + $('#add_all').click(function() { + //获取全部的选项,删除并追加给对方 + $('#select1 option').appendTo('#select2'); + }); + //全部移到左边 + $('#remove_all').click(function() { + $('#select2 option').appendTo('#select1'); + }); + + //双击选项 + $('#group_select_left').dblclick(function(){ //绑定双击事件 + //获取全部的选项,删除并追加给对方 + $("option:selected",this).appendTo('#group_select_right'); //追加给对方 + }); + //双击选项 + $('#group_select_right').dblclick(function(){ + $("option:selected",this).appendTo('#group_select_left'); + }); +}); + +</script> +{% endblock js %} diff --git a/templates/user/users.html b/templates/user/users.html new file mode 100644 index 0000000..bf7d8bf --- /dev/null +++ b/templates/user/users.html @@ -0,0 +1,140 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>用户</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">用户管理</li> + <li class="breadcrumb-item active">用户</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">用户<a href="javascript:void(0)" class="btn btn-sm btn-success ml-3 addsoft">新增 + </a></h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-hover"> + <thead> + <tr> + <th>用户名</th> + <th>昵称</th> + <th>邮箱</th> + <th>性别</th> + <th>是否启用</th> + <th>角色</th> + <th>组</th> + <th>创建时间</th> + <th>最后登陆时间</th> + <th>操作</th> + </tr> + </thead> + <tbody> + {% for user in users %} + <tr> + <td><a href="{% url 'user:user' user.id %}">{{ user.username }}</a></td> + <td>{{ user.nickname }}</td> + <td>{{ user.email }}</td> + <td>{{ user.get_sex_display }}</td> + {% if user.enabled %} + <td>启用</td> + {% else %} + <td style="color:red;">禁用</td> + {% endif %} + <td>{{ user.get_role_display }}</td> + + <td> + {% for group in user.groups.all %}<button type="button" class="btn btn-sm btn-success btn-flat">{{ group.group_name }}</button> {% endfor %} + </td> + <td>{{ user.create_time|date:"Y/m/d H:i:s" }}</td> + <td>{{ user.last_login_time|date:"Y/m/d H:i:s"|default:''}}</td> + <td> + <a href="{% url 'user:user' user.id %}">查看</a> <a href="{% url 'user:user_edit' user.id %}">修改</a> 删除 + </td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>用户名</th> + <th>昵称</th> + <th>邮箱</th> + <th>性别</th> + <th>是否启用</th> + <th>角色</th> + <th>组</th> + <th>创建时间</th> + <th>最后登陆时间</th> + <th>操作</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/webssh/hosts.html b/templates/webssh/hosts.html new file mode 100644 index 0000000..c43ddc8 --- /dev/null +++ b/templates/webssh/hosts.html @@ -0,0 +1,162 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>web终端</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">作业中心</li> + <li class="breadcrumb-item active">web终端</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">web终端</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-hover"> + <thead> + <tr> + <th>主机ID</th> + <th>类型</th> + <th>环境</th> + <th>名称</th> + <th>IP</th> + <th>公网IP</th> + <th>协议</th> + <th>端口</th> + <th>系统</th> + <th>用户名</th> + <th>添加时间</th> + <th>操作</th> + </tr> + </thead> + <tbody> + {% for host in hosts %} + <tr> + <td>{{ host.id }}</td> + <td>{{ host.host.get_type_display }}</td> + <td>{{ host.host.get_env_display }}</td> + {% if host.remote_user %} + <td>{{ host.host.hostname }}_{{ host.remote_user.name }}</td> + {% else %} + <td>{{ host.host.hostname }}</td> + {% endif %} + <td>{{ host.host.ip }}</td> + <td>{{ host.host.wip | default:'' }}</td> + <td>{{ host.host.get_protocol_display }}</td> + <td>{{ host.host.port }}</td> + <td>{{ host.host.release }}</td> + {% if host.remote_user %} + <td>{{ host.remote_user.username }}</td> + {% else %} + <td></td> + {% endif %} + <td>{{ host.create_time|date:"Y/m/d H:i:s" }}</td> + <td> + {% if host.remote_user and host.enabled %} + {% if host.host.get_protocol_display == 'ssh' %} + <form method="post" action="{% url 'webssh:terminal' %}" target="_blank"> + {% elif host.host.get_protocol_display == 'telnet' %} + <form method="post" action="{% url 'webtelnet:terminal' %}" target="_blank"> + {% else %} + <form method="post" action="#"> + {% endif %} + {% csrf_token %} + <input type="text" name="hostid" value="{{ host.id }}" hidden> + <button type="submit" class="btn btn-block btn-sm btn-success btn-flat">连接</button> + </form> + {% else %} + <button class="btn btn-block btn-sm btn-secondary btn-flat disabled">连接</button> + {% endif %} + + </td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>主机ID</th> + <th>类型</th> + <th>环境</th> + <th>名称</th> + <th>IP</th> + <th>公网IP</th> + <th>协议</th> + <th>端口</th> + <th>系统</th> + <th>用户名</th> + <th>添加时间</th> + <th>操作</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[10, 25, 50, 100, -1], [10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/templates/webssh/logs.html b/templates/webssh/logs.html new file mode 100644 index 0000000..5fe5aac --- /dev/null +++ b/templates/webssh/logs.html @@ -0,0 +1,134 @@ +{% extends 'base.html' %} +{% load static %} + + {% block title %} + <title>web终端日志</title> + {% endblock title %} + + {% block navheader %} + <section class="content-header"> + <div class="container-fluid"> + <div class="row mb-1"> + <div class="col-12"> + <ol class="breadcrumb"> + <li class="breadcrumb-item">日志审计</li> + <li class="breadcrumb-item active">web终端日志</li> + </ol> + </div> + </div> + </div><!-- /.container-fluid --> + </section> + {% endblock navheader %} + + {% block content %} + <div class="card"> + <div class="card-header"> + <h3 class="card-title">web终端日志</h3> + <div class="card-tools"> + <button type="button" class="btn btn-tool" data-widget="collapse"> + <i class="fas fa-minus"></i> + </button> + <button type="button" class="btn btn-tool" data-widget="remove"> + <i class="fas fa-times"></i> + </button> + </div> + </div> + <!-- /.card-header --> + <div class="card-body table-responsive"> + <table id="datatables-lists" class="table table-bordered table-striped table-sm"> + <thead> + <tr> + <th>操作人</th> + <th>主机名</th> + <th>主机IP</th> + <th>协议</th> + <th>端口</th> + <th>用户名</th> + <th>命令详情</th> + <th>IP地址</th> + <th>User_Agent</th> + <th>会话开始时间</th> + <th>事件时间</th> + </tr> + </thead> + <tbody> + {% for log in logs %} + <tr> + <td>{{ log.user | default:'' }}</td> + <td>{{ log.hostname }}</td> + <td>{{ log.ip }}</td> + <td>{{ log.protocol }}</td> + <td>{{ log.port }}</td> + <td>{{ log.username }}</td> + <td>{{ log.cmd | linebreaksbr | truncatechars_html:15 | default:'' }}<br>...</td> + <td>{{ log.address | default:'' }}</td> + <td>{{ log.useragent | truncatechars_html:20 | default:'' }}...</td> + <td>{{ log.start_time | date:"Y/m/d H:i:s" }}</td> + <td>{{ log.create_time | date:"Y/m/d H:i:s" }}</td> + </tr> + {% endfor %} + </tbody> + <tfoot> + <tr> + <th>操作人</th> + <th>主机名</th> + <th>主机IP</th> + <th>协议</th> + <th>端口</th> + <th>用户名</th> + <th>命令详情</th> + <th>IP地址</th> + <th>User_Agent</th> + <th>会话开始时间</th> + <th>事件时间</th> + </tr> + </tfoot> + </table> + </div> + <!-- /.card-body --> + </div> + <!-- /.card --> + {% endblock content %} + +{% block js %} +<!-- DataTables --> +<script src="{% static 'adminlte/plugins/datatables/jquery.dataTables.js' %}"></script> +<script src="{% static 'adminlte/plugins/datatables/dataTables.bootstrap4.js' %}"></script> +<script> +$("#datatables-lists").DataTable({ + language: { + "sProcessing": "处理中...", + "sLengthMenu": "显示 _MENU_ 项结果", + "sZeroRecords": "没有匹配结果", + "sInfo": "显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项", + "sInfoEmpty": "显示第 0 至 0 项结果,共 0 项", + "sInfoFiltered": "(由 _MAX_ 项结果过滤)", + "sInfoPostFix": "", + "sSearch": "搜索:", + "sUrl": "", + "sEmptyTable": "表中数据为空", + "sLoadingRecords": "载入中...", + "sInfoThousands": ",", + "oPaginate": { + "sFirst": "首页", + "sPrevious": "上页", + "sNext": "下页", + "sLast": "末页" + }, + "oAria": { + "sSortAscending": ": 以升序排列此列", + "sSortDescending": ": 以降序排列此列" + } + }, + destroy: true, // 允许重建 + bProcessing:true, // 表格数据过多处理时显示: sProcessing + lengthMenu: [[5, 10, 25, 50, 100, -1], [5, 10, 25, 50, 100, "全部"]], + order: [], + //scrollY: 480, // 滚动条 + //scrollCollapse: true, + //jQueryUI: true, + stateSave: true, // 保存最后一次分页信息、排序信息,当页面刷新,或者重新进入这个页面,恢复上次的状态。 + stateDuration: 86400, // 本地储存(0~更大)还是session储存(-1) +}); +</script> +{% endblock js %} \ No newline at end of file diff --git a/upload_to_server.py b/upload_to_server.py new file mode 100644 index 0000000..5669dad --- /dev/null +++ b/upload_to_server.py @@ -0,0 +1,30 @@ +import paramiko + + +def upload(now, total): + print('\r总大:{0} 上传:{1}'.format(total, now), end='') + if total <= now: + print() + + +# 实例化一个transport对象 +trans = paramiko.Transport(('192.168.223.111', 22)) + +# 建立连接 +trans.connect(username='root', password='123456') + +# 实例化一个 sftp对象,指定连接的通道 +sftp = paramiko.SFTPClient.from_transport(trans) + +# 发送文件 +sftp.put(localpath='./devops.zip', remotepath='/root/devops.zip', callback=upload) + +# 将sshclient的对象的transport指定为以上的trans +ssh = paramiko.SSHClient() +ssh._transport = trans +# 执行命令,和传统方法一样 +stdin, stdout, stderr = ssh.exec_command('cd /root;unzip -oq devops.zip;cd devops;/bin/sh start_docker.sh') +print(stdout.read().decode()) + +# 关闭连接 +trans.close() diff --git a/user/__pycache__/forms.cpython-37.pyc b/user/__pycache__/forms.cpython-37.pyc index 80e6f541cbd937018ac7b84f574b3d642b38a093..da56575e722de9fd89489e20799de30254414054 100644 GIT binary patch literal 2238 zcmaKtTW=dh6vy{!udi{Pq&M#O?h8WV0SO@#HA-ovDs2f!)hfu^W~R=D^{z8()AR)u zC{-gADL{n^;(<y)K&Wm3L8Ws09`g#eV~J0|6aO>oT-sROm5=9~Idf*_%<s$|EtT>b zp2dHs-@2L8w7*H&dMq@i@Jr4@FpcS<*3dm&*Iv_@!OY7VGr7KGd|-MeEEcn2v4<85 zmJG|nk{w!XSR9svB{#HWSbk1(3j;*gTwU7dW6?+!NZWccXiVXkya2&9PiLBEaC6CG zI<0_NYo=!_-KN>l$}nfm^0F+4$a$GN8P|j87~;0EIjw)~yWZ71-SxGB4s9?6;hNG8 z{c}8&`9=`cLLSvyi)i~R!*-k2(7kn~|J8c~Y7R7vbuB?{ktN|k$jpU+)p={yb)-|P zMSg?VYBFD|HDcBdsa>qqmfL=q_6UQ<<fa#WaV+2=lTJI~g8F3Xw4WpwV!=*C&2~#V z&7u0eCdO$t68{~Ud3nBhv9%aSFTPZ3ig<}HwB{LqCvGP56lq?i^7-aPVZxoxF(Kg? z6<t|IC;2#B_9hugwUKDc%M25Z#BbXu<rY%E+vcz=lJCe0wing8+7DA_P{ah?KR)fQ zuB8bTbSQ!V+>=mIfK?q@@3Y&fcY6$`{*BIN=j(0TteX=QRg_4SNl<D9xr%)dUM>!q z$|3V|5x+2OmbaUJ-4CLj)TXTo8a@d@5feIUN1wo}Y@?I+E}XWK$8^u+7Sfh7Y9Mvi zQkH?yHQjSmPZqU+YG8RJHm58F@SI~s5S>?gX<IKqFS0T>+1Q%ll~k|6D)=vB#6CnA zV^ySlTu!Dbe-j9t7V-HY<dk>Ua1CiS{E)W^hyJxY&@<=xwx#>c&kFP2^<VlMw}lNS z*GLH`xK-ca{Z7C0qpK@(>b?3=@5{RbI;dlN&QCgmFSi51Sz17UcK-S6hHLHw6^AiS z8fVU=u`X?Nul_KcRAv_z<6wa&_n<*5QB<grvu@?SgO`Q5nX|R&<EKtc&&=&mAe|^! zcw3cLex~6EVOnrDb{~Y&ZlZ=bs+R{VK_rdkWm%Y3lc#QG!dIm82489Ug8N7!Z^R>E z*^ku`HAw}+F)XvJ7xk)M!CTe^^^V5ocl7MWh8dp4ZCqox+?b8y&MGZ4)EsCz26zBc zUiAPWyaj+`c?D%D!MDK50I8_-v2DEsy$o<nKx24gsyD-`_^)8ZI3nyryUHe@O~PYb z`S0hJ@}E%t2iU=3^htII&>fcL9q6`b69=%Z9oWRdEfu5@je>3?RnMEhe%Qae(eK>o zuV3kYeBCt$RKIPi>E7p^!KE+KJNw@2do-aby$^&{975c^uqvV+#Ho0Q7De|!Z#Sso zVOSmjX}XQ;x4P@!cGquVCtFoT?5l8_8=q{h{<Ld%{ogJJ+7V5gb@$)Lw|E4DWbyS= zCujCjE(kmED2c~NJWk>X5>JwNip0|-o+0rpiRU0>@n{@}+>cV)vpn+8g`7!Cz$!C! z5x1MkNZzqTjY22174U8m@w=IyfClo(Z`RGq3|qp-AWlDg7IC6JMCw9S<)J)BO(mUI V(vRq?^wCb}8Yr68d^J-o{|}^^8zle$ delta 87 zcmdldc$<ySiI<m)0SLZ)c8E1*p2#P`XfjdVm@S1Zm_d_$V@oWP_$|)-oRosZ;^Oj@ kTby~R<xuA2A51pPewwV4GdScXPvB7EU;&!K!z{!F08$zljsO4v diff --git a/user/__pycache__/models.cpython-37.pyc b/user/__pycache__/models.cpython-37.pyc index 4a24adb2934783ec336f75220a9d35b22e097484..372f835694f258ebba507cd5c300db8e5fd0ae83 100644 GIT binary patch delta 863 zcmbOw*DKHG#LLUY00eiYImJHZpUAhGNs4je?uGSiDbgt{Kq!;K(aRDgn#!8Rmck4c zkp+v0fkouNBJyAnaiE9<n6Hqc*vlLx3FJ!w`AR9uy-ZQkP##N)3RqSqMKwjOg&|7T zogqa%MWclwMWdN1N-jk+m_bwP7R%<PjH?+{UUCCnR>}q>o^^IVo3jhde7bMlll=={ zN&rQpm;wVQ2QY79l$^}WV$P^I*_vgajU-T_2th~zxwlw+Q%e$6fUIJWVH}KXj4X^r zia@5HrhJhMkS>x15pt7FS@qe(Kuqz;S*$%mtOXhQd8tus<*At!nR!u6g@u#9vf67% zgH(XDBiRGC5oB2r3y@$3;$pSQF>KPTLX1GTxr~jMkx_JV3%i>n$mK{H!FoU@8ccr7 zUR{sm8jw195CIB?A_WkE@E5{9kVA}t7}Ib3$bJK9X95vQAOfVJNDV|Vg9vpHp$8<4 zi}XQE0}x>dBJlfF2&hDBvjFE+My4X+&Cj`7L7vRzDO807B2ZS7u?Uxk!zMHHc0`ir z;b@@Im>w2E_VDB<yhZ{pAU|k<2v-oHH(8BO#}*U<w>V3SQ;QPQQ}arQ_N@s}pXKC! aK50gi$*cKnL_vIvD4G0+Pnua-h#vq6Yna^t delta 767 zcmeB`pC!lV#LLUY00djY9Aj_sP2^k6^n_vJ?uG86DJ(!Jog&lA93_^@nk5e8$)?El zGDS&1c`PaNU|Go&g%rgWhA1g_h7_d~<ran%<z}WR=@gY<22It?HyPJ5PF}^liBWuV zEQ>j#+~lb&`)tI478W502_W|ti*IU4q9Tx0%mySl7}*$E7>neAOg~MTA}JtUBn={D zCik=Ivx$J1qLYuX_DoJ@v(u0SaY1U43<VnxGP#HaNU#HOvBKn&Y|@)wvGFi63Qzvd z?j|XQss*eXWR~vaFplbaBuhc+WI+Tsh>!yj2<Ibg0y)6|h%vpukL(4Ib|w&^03tvd zic~-ZGl)<H5!yh)xJU=Y)CCcGAOgQHg@8)bH#c)$Wn_BEy*Y}z739eWJcX)|umZ|z zG8W<TaPZ_Z-i}D}JRAWu8q>of$R3^?&Sxax1oDFhh;RlG+LLGU=|n03nYTDgi&Kjd z(^K<Gi1w`^P@fqP7lXXb!NejW#wftZ2gE{*d`ujyMTV2*_-#Z%sxTsIaz4K_vw{#m E0BbFI$p8QV diff --git a/user/__pycache__/urls.cpython-37.pyc b/user/__pycache__/urls.cpython-37.pyc index 9e6fe78e61d6b00a5fb46dfc836c57dedbfa9568..8b89dd0133137db2259b9b7cf9577079bbeba8b5 100644 GIT binary patch delta 312 zcmbQm@`gp-iI<m)0SF>bJIC5HF)%y^abSQK$Z!DS;zbkH8`V-7Q$$jkvv^WiQrLT$ zfjm(lk2ggO#7pOl;!6<^X3&(FxXGKRv^ce>SpOC)h@Q;Js8G*QP?VpRnUku2iyg|U z;)MyNrev1r-{OIC<3ZdiVUSLJo6NitD-bI_GsRB7N&<@@L^CH?5s2-l$$yJGB`Yy6 zJzuZ1D5tmx=p}|*lM5I%wTgIv%p#DNidaDeA4rB9sHh;Zq$IT{uXrUxkuXR?c=Aa` TYbh}xlZTOqnT3gu5fuUeF2Pgx delta 211 zcmaFEGK)psiI<m)0SG4ja)`am$iVOz#DM{BAj1KOi`youH*%%3NAaYH1T$!gPCVz$ zlapCoQmlWA6+}-~WmGWXP0mQnOHVCGEG{lj(U0Om=2mf*7N-_v=B4H9-{OEV{WN(e zZ)CJFh=S^jhiHugtBnUKzQtWylv9vcQj%JfSG<y;ND%0jBEiYxOx9vTKqe0(4>JoB IA0q++05D=Y8vp<R diff --git a/user/__pycache__/urls_api.cpython-37.pyc b/user/__pycache__/urls_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a9bf0060382fe620a30ff8f7a3f49f3c017701e3 GIT binary patch literal 414 zcmX|;%}&EG49AnETidnkc#R2ZJ8(ckfDnl5F5If3D#Bq~)sHA?H?0TWfCu44c!*v( z?G-p-i*AwP_~*Z6JIUj8I$^jzzMo$2IAcEqr{$sZfZMLn00UmIk|&%qa6lAJDH4Ic zo4AU@NQry|WAHu&l8-coV4w+-2O-f~BtAqhY(70=O*ZGft7Wq}Vrzn6w^e(aNxkje z)+*CoXld=fHn40p$VM&u*(s9!N2o+4OV!P^so%GSI%md1U`XsDs(4mIqCr~CgFb{@ zR_l7vn!*|f$LX1I>F}l28FesYMB|eQNC+_%hi{ar>=!dJ31ogRpKnrqXf}0qb)Df5 pxq53-P`g^&l)9$WgD$zyhu*&}YiJ8~PfuX!Ga?=d*NJ(|{{X@GbO`_e literal 0 HcmV?d00001 diff --git a/user/__pycache__/views.cpython-37.pyc b/user/__pycache__/views.cpython-37.pyc index 477372deeda1b9d78205a4f6bbfd66ea16e70968..9bc8a1588aed1ea4b6d92d22477d7af0be227076 100644 GIT binary patch literal 4327 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 literal 5000 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 Olrsd>x*w0d@%$UVsvN`s diff --git a/user/__pycache__/views_api.cpython-37.pyc b/user/__pycache__/views_api.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a417a58324f9696b6462f987bef84f9dad1a425 GIT binary patch literal 4490 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 literal 0 HcmV?d00001 diff --git a/user/forms.py b/user/forms.py index 011607a..47ba030 100644 --- a/user/forms.py +++ b/user/forms.py @@ -15,3 +15,40 @@ class ChangePasswdForm(forms.Form): oldpasswd = forms.CharField(label="当前密码", min_length=6, max_length=256, widget=forms.PasswordInput) newpasswd = forms.CharField(label="新密码", min_length=6, max_length=256, widget=forms.PasswordInput) newpasswdagain = forms.CharField(label="确认新密码", min_length=6, max_length=256, widget=forms.PasswordInput) + + +class ChangeUserProfileForm(forms.Form): + SEX_CHOICES = ( + ('male', "男"), + ('female', "女"), + ) + nickname = forms.CharField(label="昵称", max_length=64) + email = forms.EmailField(label="邮箱") + phone = forms.CharField(label="手机", min_length=11, max_length=11, required=False) + weixin = forms.CharField(label="微信", max_length=64, required=False) + qq = forms.CharField(label="QQ", max_length=64, required=False) + sex = forms.ChoiceField(label="性别", choices=SEX_CHOICES) + memo = forms.CharField(label="昵称", max_length=256, widget=forms.Textarea, required=False) + + +class ChangeUserForm(forms.Form): + SEX_CHOICES = ( + ('male', "男"), + ('female', "女"), + ) + ROLE_CHOICES = ( + (1, '超级管理员'), + (2, '普通用户'), + ) + username = forms.CharField(label="用户名", max_length=64) + nickname = forms.CharField(label="昵称", max_length=64) + email = forms.EmailField(label="邮箱") + phone = forms.CharField(label="手机", min_length=11, max_length=11, required=False) + weixin = forms.CharField(label="微信", max_length=64, required=False) + qq = forms.CharField(label="QQ", max_length=64, required=False) + sex = forms.ChoiceField(label="性别", choices=SEX_CHOICES) + memo = forms.CharField(label="昵称", max_length=256, widget=forms.Textarea, required=False) + enabled = forms.BooleanField(label="是否启用", required=False) + role = forms.ChoiceField(label="角色", choices=ROLE_CHOICES) + groups = forms.CharField(label="用户组", max_length=10240, required=False) + diff --git a/user/migrations/0004_auto_20190801_1537.py b/user/migrations/0004_auto_20190801_1537.py new file mode 100644 index 0000000..75b2a63 --- /dev/null +++ b/user/migrations/0004_auto_20190801_1537.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.3 on 2019-08-01 15:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0003_auto_20190731_1655'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='phone', + field=models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号'), + ), + migrations.AddField( + model_name='user', + name='qq', + field=models.CharField(blank=True, max_length=24, null=True, verbose_name='QQ'), + ), + migrations.AddField( + model_name='user', + name='weixin', + field=models.CharField(blank=True, max_length=64, null=True, verbose_name='微信'), + ), + ] diff --git a/user/migrations/__pycache__/0004_auto_20190801_1537.cpython-37.pyc b/user/migrations/__pycache__/0004_auto_20190801_1537.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c6d90a51bb7f9304c5e96da2b156cf087dade5b4 GIT binary patch literal 718 zcmZ8fJ&)5s5M6t1pY2QF3M3GM7AeiiA%p-zC>#n@oOFW93R*dtA+d36#~;UC4Glsk zIw~4U8YC(t3iuz}!kv`AK*g*t)&XnncxK;t_RSmbwA(JD_2vD}vk_<PrwW>7kTaxR zTq6Svw3sb4Ut8$DuCYf9bl|TUaMU9Hiu)XlC#++xD3^6~=`O-q9+Y946w+FxfN?QZ zP_s748PYByGRS-ln6HC2(|nFbWUAt5MH^s%N!x;rBLnQC26NV!OW5XrSZj^72|L$d zGw$1Po(OJ9=W)Y~Xc|?4xxDCkUQYy7nTqRP_qI3abw&4Pzn_~#PMX;~P4MYTQLY>^ zCvV=KeE4?!?rX=8<|Gc17u0TWAYx2r<y;y`700r*kNG4mP$a<uJDf6<2z5jmbSBN` zA;uu}<#KgN-J|dAN$Vws2Vt@rs=F*Q$KOBy`tfnM<49Z79}6K}A*fgtBl#^MmQ@g| zf+_;cFab=aAr`r=s@nGf9;%CSnlF>igL#nGv2@c6*CYDG@+*Ya=J#lHXPh0D)O2qk zvOJA&T8;tt)2tXPxyEOs9g}c3{u1t%N5b`cgYE1vSHF+yptwxN=9Xb-Iz1Qkf8TZ9 mE-C3i6eP2B8zwdJnr;oAQ9JdnQax$kJ(b{|>Z(wkZt*`oI=#LC literal 0 HcmV?d00001 diff --git a/user/models.py b/user/models.py index 062ecae..cc22b5b 100644 --- a/user/models.py +++ b/user/models.py @@ -23,6 +23,9 @@ class User(models.Model): enabled = models.BooleanField(default=True, verbose_name='是否启用') role = models.SmallIntegerField(default=2, choices=ROLE_CHOICES, verbose_name='角色') groups = models.ManyToManyField('Group', blank=True, verbose_name="所属组") + phone = models.CharField(max_length=11, blank=True, null=True, verbose_name="手机") + weixin = models.CharField(max_length=64, blank=True, null=True, verbose_name="微信") + qq = models.CharField(max_length=24, blank=True, null=True, verbose_name="QQ") memo = models.TextField(blank=True, null=True, verbose_name="备注") create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') last_login_time = models.DateTimeField(blank=True, null=True, verbose_name='最后登录时间') diff --git a/user/urls.py b/user/urls.py index 9c6b1d3..9252ecc 100644 --- a/user/urls.py +++ b/user/urls.py @@ -1,15 +1,16 @@ -from django.urls import path -from . import views - - -app_name = 'user' -urlpatterns = [ - path('login/', views.login, name='login'), - path('logout/', views.logout, name='logout'), - path('lists/', views.lists, name='lists'), - path('groups/', views.groups, name='groups'), - path('logs/', views.logs, name='logs'), - path('changepasswd/', views.change_passwd, name='changepasswd'), - path('userinfo/', views.user_info, name='userinfo'), -] - +from django.urls import path +from . import views + + +app_name="user" +urlpatterns = [ + path('login/', views.login, name='login'), + path('logout/', views.logout, name='logout'), + path('users/', views.users, name='users'), + path('groups/', views.groups, name='groups'), + path('logs/', views.logs, name='logs'), + path('profile/', views.profile, name='profile'), + path('profile/edit/', views.profile_edit, name='profile_edit'), + path('user/<int:user_id>/', views.user, name='user'), + path('user/<int:user_id>/edit/', views.user_edit, name='user_edit'), +] diff --git a/user/urls_api.py b/user/urls_api.py new file mode 100644 index 0000000..483a46b --- /dev/null +++ b/user/urls_api.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views_api + + +app_name="user" +urlpatterns = [ + path('password/update/', views_api.password_update, name='password_update'), + path('profile/update/', views_api.profile_update, name='profile_update'), + path('user/update/', views_api.user_update, name='user_update'), +] diff --git a/user/views.py b/user/views.py index 9eef4f5..fd5e196 100644 --- a/user/views.py +++ b/user/views.py @@ -1,158 +1,138 @@ -from django.shortcuts import render, redirect -from django.urls import reverse -from django.http import JsonResponse -from .models import User, LoginLog, Group -from .forms import LoginForm, ChangePasswdForm -from util.tool import login_required, hash_code, post_required, admin_required -import django.utils.timezone as timezone -import time -import traceback -# Create your views here. - - -def login_event_log(user, event_type, detail, address, useragent): - event = LoginLog() - event.user = user - event.event_type = event_type - event.detail = detail - event.address = address - event.useragent = useragent - event.save() - - -def login(request): - if request.session.get('islogin', None): # 不允许重复登录 - return redirect(reverse('server:index')) - if request.method == "POST": - login_form = LoginForm(request.POST) - error_message = '请检查填写的内容!' - if login_form.is_valid(): - username = login_form.cleaned_data.get('username') - password = login_form.cleaned_data.get('password') - try: - user = User.objects.get(username=username) - if not user.enabled: - error_message = '用户已禁用!' - login_event_log(user, 3, '用户 {} 已禁用'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return render(request, 'user/login.html', locals()) - except BaseException: - error_message = '用户不存在!' - login_event_log(None, 3, '用户 {} 不存在'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return render(request, 'user/login.html', locals()) - # if user.password == password: - if user.password == hash_code(password): - data = {'last_login_time': timezone.now()} - User.objects.filter(username=username).update(**data) - request.session.set_expiry(0) - request.session['issuperuser'] = False - if user.role == 1: # 超级管理员 - request.session['issuperuser'] = True - request.session['islogin'] = True - request.session['userid'] = user.id - request.session['username'] = user.username - request.session['nickname'] = user.nickname - now = int(time.time()) - request.session['logintime'] = now - request.session['lasttime'] = now - login_event_log(user, 1, '用户 {} 登陆成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return redirect(reverse('server:index')) - else: - error_message = '密码错误!' - login_event_log(user, 3, '用户 {} 密码错误'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return render(request, 'user/login.html', locals()) - else: - login_event_log(None, 3, '登陆表单验证错误', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return render(request, 'user/login.html', locals()) - return render(request, 'user/login.html') - - -def logout(request): - if not request.session.get('islogin', None): - return redirect(reverse('user:login')) - user = User.objects.get(id=int(request.session.get('userid'))) - # request.session.flush() # 清除所有后包括django-admin登陆状态也会被清除 - # 或者使用下面的方法 - try: - del request.session['issuperuser'] - del request.session['islogin'] - del request.session['userid'] - del request.session['username'] - del request.session['nickname'] - del request.session['logintime'] - del request.session['lasttime'] - except BaseException: - pass - login_event_log(user, 2, '用户 {} 退出'.format(user.username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return redirect(reverse('user:login')) - - -@login_required -@post_required -def change_passwd(request): - changepasswd_form = ChangePasswdForm(request.POST) - if changepasswd_form.is_valid(): - username = request.session.get('username') - oldpassword = changepasswd_form.cleaned_data.get('oldpasswd') - newpasswd = changepasswd_form.cleaned_data.get('newpasswd') - newpasswdagain = changepasswd_form.cleaned_data.get('newpasswdagain') - try: - user = User.objects.get(username=username) - if not user.enabled: - error_message = '用户已禁用!' - login_event_log(user, 4, '用户 {} 已禁用'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 401, "err": error_message}) - if newpasswd != newpasswdagain: - error_message = '两次输入的新密码不一致' - login_event_log(user, 4, '两次输入的新密码不一致', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 400, "err": error_message}) - except: - error_message = '用户不存在!' - login_event_log(None, 4, '用户 {} 不存在'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 403, "err": error_message}) - if user.password == hash_code(oldpassword): - data = {'password': hash_code(newpasswd)} - User.objects.filter(username=username).update(**data) - login_event_log(user, 5, '用户 {} 修改密码成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 200, "err": ""}) - else: - error_message = '当前密码错误!' - login_event_log(user, 4, '用户 {} 当前密码错误'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 404, "err": error_message}) - else: - error_message = '请检查填写的内容!' - user = User.objects.get(username=request.session.get('username')) - login_event_log(user, 4, '修改密码表单验证错误', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) - return JsonResponse({"code": 406, "err": error_message}) - - -@login_required -@admin_required -def lists(request): - users = User.objects.exclude(pk=request.session['userid']) # exclude 排除当前登陆用户 - return render(request, 'user/user_lists.html', locals()) - - -@login_required -@admin_required -def groups(request): - groups = Group.objects.all() - return render(request, 'user/group_lists.html', locals()) - - -@login_required -@admin_required -def logs(request): - logs = LoginLog.objects.all() - return render(request, 'user/user_logs.html', locals()) - - -@login_required -def user_info(request): - username = request.session.get('username') - user = User.objects.filter(username=username).values( - 'id', 'username', 'nickname', 'email', 'sex', 'enabled', 'role', 'groups', 'memo', 'create_time' - ) - user_info = dict(user[0]) - user_info['create_time'] = user[0]['create_time'].strftime('%Y/%m/%d %H:%M:%S') - return JsonResponse({"code": 200, "user": user_info}) - +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse +from django.http import JsonResponse +from .models import User, LoginLog, Group +from .forms import LoginForm, ChangePasswdForm, ChangeUserProfileForm, ChangeUserForm +from util.tool import login_required, hash_code, post_required, admin_required +import django.utils.timezone as timezone +from django.db.models import Q +import time +import traceback +# Create your views here. + + +def login_event_log(user, event_type, detail, address, useragent): + event = LoginLog() + event.user = user + event.event_type = event_type + event.detail = detail + event.address = address + event.useragent = useragent + event.save() + + +def login(request): + if request.session.get('islogin', None): # 不允许重复登录 + return redirect(reverse('server:index')) + if request.method == "POST": + login_form = LoginForm(request.POST) + error_message = '请检查填写的内容!' + if login_form.is_valid(): + username = login_form.cleaned_data.get('username') + password = login_form.cleaned_data.get('password') + try: + user = User.objects.get(username=username) + if not user.enabled: + error_message = '用户已禁用!' + login_event_log(user, 3, '用户 {} 已禁用'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return render(request, 'user/login.html', locals()) + except BaseException: + error_message = '用户不存在!' + login_event_log(None, 3, '用户 {} 不存在'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return render(request, 'user/login.html', locals()) + # if user.password == password: + if user.password == hash_code(password): + data = {'last_login_time': timezone.now()} + User.objects.filter(username=username).update(**data) + request.session.set_expiry(0) + request.session['issuperuser'] = False + if user.role == 1: # 超级管理员 + request.session['issuperuser'] = True + request.session['islogin'] = True + request.session['userid'] = user.id + request.session['username'] = user.username + request.session['nickname'] = user.nickname + now = int(time.time()) + request.session['logintime'] = now + request.session['lasttime'] = now + login_event_log(user, 1, '用户 {} 登陆成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return redirect(reverse('server:index')) + else: + error_message = '密码错误!' + login_event_log(user, 3, '用户 {} 密码错误'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return render(request, 'user/login.html', locals()) + else: + login_event_log(None, 3, '登陆表单验证错误', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return render(request, 'user/login.html', locals()) + return render(request, 'user/login.html') + + +def logout(request): + if not request.session.get('islogin', None): + return redirect(reverse('user:login')) + user = User.objects.get(id=int(request.session.get('userid'))) + # request.session.flush() # 清除所有后包括django-admin登陆状态也会被清除 + # 或者使用下面的方法 + try: + del request.session['issuperuser'] + del request.session['islogin'] + del request.session['userid'] + del request.session['username'] + del request.session['nickname'] + del request.session['logintime'] + del request.session['lasttime'] + except BaseException: + pass + login_event_log(user, 2, '用户 {} 退出'.format(user.username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return redirect(reverse('user:login')) + + +@login_required +@admin_required +def users(request): + users = User.objects.exclude(pk=request.session['userid']) # exclude 排除当前登陆用户 + return render(request, 'user/users.html', locals()) + + +@login_required +@admin_required +def groups(request): + groups = Group.objects.all() + return render(request, 'user/groups.html', locals()) + + +@login_required +@admin_required +def logs(request): + logs = LoginLog.objects.all() + return render(request, 'user/logs.html', locals()) + + +@login_required +def profile(request): + user = get_object_or_404(User, pk=request.session.get('userid')) + return render(request, 'user/profile.html', locals()) + + +@login_required +def profile_edit(request): + user = get_object_or_404(User, pk=request.session.get('userid')) + return render(request, 'user/profile_edit.html', locals()) + + +@login_required +@admin_required +def user(request, user_id): + user = get_object_or_404(User, pk=user_id) + return render(request, 'user/user.html', locals()) + + +@login_required +@admin_required +def user_edit(request, user_id): + user = get_object_or_404(User, pk=user_id) + other_groups = Group.objects.filter( # 查询当前用户不属于的组 + ~Q(user__id = user_id), + ) + return render(request, 'user/user_edit.html', locals()) + diff --git a/user/views_api.py b/user/views_api.py new file mode 100644 index 0000000..4182486 --- /dev/null +++ b/user/views_api.py @@ -0,0 +1,157 @@ +from django.shortcuts import render, redirect, get_object_or_404 +from django.urls import reverse +from django.http import JsonResponse +from .models import User, LoginLog, Group +from .forms import LoginForm, ChangePasswdForm, ChangeUserProfileForm, ChangeUserForm +from util.tool import login_required, hash_code, post_required, admin_required +import django.utils.timezone as timezone +import time +import traceback +# Create your views here. + + +def login_event_log(user, event_type, detail, address, useragent): + event = LoginLog() + event.user = user + event.event_type = event_type + event.detail = detail + event.address = address + event.useragent = useragent + event.save() + + +@login_required +@post_required +def password_update(request): + changepasswd_form = ChangePasswdForm(request.POST) + if changepasswd_form.is_valid(): + username = request.session.get('username') + oldpassword = changepasswd_form.cleaned_data.get('oldpasswd') + newpasswd = changepasswd_form.cleaned_data.get('newpasswd') + newpasswdagain = changepasswd_form.cleaned_data.get('newpasswdagain') + try: + user = User.objects.get(username=username) + if not user.enabled: + error_message = '用户已禁用!' + login_event_log(user, 4, '用户 {} 已禁用'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 401, "err": error_message}) + if newpasswd != newpasswdagain: + error_message = '两次输入的新密码不一致' + login_event_log(user, 4, '两次输入的新密码不一致', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 400, "err": error_message}) + except: + error_message = '用户不存在!' + login_event_log(None, 4, '用户 {} 不存在'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 403, "err": error_message}) + if user.password == hash_code(oldpassword): + data = {'password': hash_code(newpasswd)} + User.objects.filter(username=username).update(**data) + login_event_log(user, 5, '用户 {} 修改密码成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 200, "err": ""}) + else: + error_message = '当前密码错误!' + login_event_log(user, 4, '用户 {} 当前密码错误'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 404, "err": error_message}) + else: + error_message = '请检查填写的内容!' + user = User.objects.get(username=request.session.get('username')) + login_event_log(user, 4, '修改密码表单验证错误', request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 406, "err": error_message}) + + +@login_required +@post_required +def profile_update(request): + changeuserprofile_form = ChangeUserProfileForm(request.POST) + if changeuserprofile_form.is_valid(): + username = request.session.get('username') + nickname = changeuserprofile_form.cleaned_data.get('nickname') + email = changeuserprofile_form.cleaned_data.get('email') + phone = changeuserprofile_form.cleaned_data.get('phone') + weixin = changeuserprofile_form.cleaned_data.get('weixin') + qq = changeuserprofile_form.cleaned_data.get('qq') + sex = changeuserprofile_form.cleaned_data.get('sex') + memo = changeuserprofile_form.cleaned_data.get('memo') + data = { + 'nickname': nickname, + 'email': email, + 'phone': phone, + 'weixin': weixin, + 'qq': qq, + 'sex': sex, + 'memo': memo, + } + try: + user = User.objects.get(username=username) + if not user.enabled: + error_message = '用户已禁用!' + return JsonResponse({"code": 401, "err": error_message}) + User.objects.filter(username=username).update(**data) + request.session['nickname'] = nickname + login_event_log(user, 10, '用户 {} 更新个人信息成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 200, "err": ""}) + except: + error_message = '用户不存在!' + return JsonResponse({"code": 402, "err": error_message}) + else: + error_message = '请检查填写的内容!' + return JsonResponse({"code": 403, "err": error_message}) + + +@login_required +@admin_required +@post_required +def user_update(request): + changeuser_form = ChangeUserForm(request.POST) + if changeuser_form.is_valid(): + log_user = request.session.get('username') + username = changeuser_form.cleaned_data.get('username') + nickname = changeuser_form.cleaned_data.get('nickname') + email = changeuser_form.cleaned_data.get('email') + phone = changeuser_form.cleaned_data.get('phone') + weixin = changeuser_form.cleaned_data.get('weixin') + qq = changeuser_form.cleaned_data.get('qq') + sex = changeuser_form.cleaned_data.get('sex') + memo = changeuser_form.cleaned_data.get('memo') + enabled = changeuser_form.cleaned_data.get('enabled') + role = changeuser_form.cleaned_data.get('role') + groups = changeuser_form.cleaned_data.get('groups') + if groups: + try: + groups = [int(group) for group in groups.split(',')] + except: + error_message = '请检查填写的内容!' + return JsonResponse({"code": 401, "err": error_message}) + else: + groups = None + data = { + 'nickname': nickname, + 'email': email, + 'phone': phone, + 'weixin': weixin, + 'qq': qq, + 'sex': sex, + 'memo': memo, + 'enabled': enabled, + 'role': role, + } + try: + user = User.objects.get(username=log_user) + User.objects.filter(username=username).update(**data) + update_user = User.objects.get(username=username) + if groups: # 更新多对多字段 + update_groups = Group.objects.filter(id__in=groups) + update_user.groups.set(update_groups) + else: + update_user.groups.set(None) + update_user.save() + login_event_log(user, 10, '用户 {} 更新信息成功'.format(username), request.META.get('REMOTE_ADDR', None), request.META.get('HTTP_USER_AGENT', None)) + return JsonResponse({"code": 200, "err": ""}) + except: + # print(traceback.format_exc()) + error_message = '用户不存在!' + return JsonResponse({"code": 402, "err": error_message}) + else: + error_message = '请检查填写的内容!' + return JsonResponse({"code": 403, "err": error_message}) + diff --git a/webssh/__pycache__/urls.cpython-37.pyc b/webssh/__pycache__/urls.cpython-37.pyc index 50b9df5704066c2fe42aff84ee2467efd4573d63..5899f4204be0de8b25e69acf0b668b7341badd07 100644 GIT binary patch delta 35 pcmaFE^oEJoiI<m)0SHXaI>&yS$UBKEBfq$$SpODlM*hT=hXJ|t3^V`$ delta 35 pcmaFE^oEJoiI<m)0SNAN+Qt5u$UBKEC$qSuSpODlPUggwhXKNP43Pi; diff --git a/webssh/__pycache__/views.cpython-37.pyc b/webssh/__pycache__/views.cpython-37.pyc index a038d366a14f33c94d8c65325a2c0d586121ce18..fe8ec010b4e893b1f8c54dee064d7261c6fd9927 100644 GIT binary patch delta 696 zcmYjPOKTKC5bo-EKX*4V`|zky8HgJ^3gRIFiI<>wTa>Vy?(EJokI+4f5E6o{M{`)l zACTO=dsOi3ADDk4c(8hQS+}7+>Z_{hud1ehIDg#mAP8K7NB%ziU>b|C3ttvbj$RXH zC4L$V0w7Fdb`qwIK?4Z4n8V!9WDbKSx46wb<|~H0h1_8Q3s-!LHQte^x%hT<4R0f; z?4w*3uX2%|g3`l7(M+^MH2DVMl1vGM9ue&Vru0_3BUg~^)xKUBb(Z$9pBva<`VAbA zJGeDfbzxl+W)$|E%r#Uuk1H>h@8#^Ai<#u2vK3~-lw(7o@=s1SB0FP|&(4>j|A~v% zM?R8rvOmEAWPehmN#s<P<Wk0YR(^u*PGz3ONx_9stE_Am%OcKBi}EWxfNuF6_V0@h zW;``$y>04(Pfd7CMW_4=zrZVh(!EX#^FnP!>?m*$B15!GO?&#Rt0Fb@(5q}zOXF;q zw2{dWfoLx1V1o}KgjDR7L+$yfxoSPh$E(f_VPLJOrM8r}){Cf6msPqdUP`U?#3Rf+ zM3Xv#lBy%e(ROL*JZrM)FdOH6HtMH2<B41@!Z^d<ErlApo?q-LPhGWf{aT~4U*N~H Zguhe|M5<nVoW_THHTt^mQ3!ns{{T9inezYu delta 494 zcmYjNO-sW-5Y289^U=*VEv4$A9=u4Pws;W~Kfr_ZB#3w^l+t!%(~>l>o8n0n^beH$ z2M>aG5yXT4BLBpL;A~oh3;US&c4pqpe%qfGJ#!pef%EZl+WS!#bOSvsb~?u@GeQzM zT?Z+?!Axd7DUYa2eZx1I%}SC1F9Ww2Va|%Xj9w{TdGU5<nVnc9=P{2`s_i%HxBgIw zpdEnN4<{lKtssd)Pv<IlXGT1n`lCc->L3i$L%faLw1?aEOwV)WfI^c~VjJ;l`i#Gj zole!}dI>a+B{&i+0O(26S9N#Kl}Zj3h%z(rc{G^~LMaIfpr|&|kckMQh+^JIOWMKx z=4#Tv?0y)Ji(z#r;<+c0QDN+1E*oZ=OkR2x#(WE28gQ2zfM>2;c$Lg$)4^;Uw?q*0 zWHe6%$LYIvd9B~TzX>ka=2Z!@Jg@rKyD-SiqbO$c&_6-ar3OG?CxU7cHL`{gB^dnz D?bK+S diff --git a/webssh/__pycache__/websocket.cpython-37.pyc b/webssh/__pycache__/websocket.cpython-37.pyc index 306f12d715929ea7c3d0a789e2f6a323da4a7b76..5e2a85d73b4f002c1eba41152b9aa75233303c31 100644 GIT binary patch delta 2048 zcmZ8iO^g&p6t3?1>-nGA`QO=}c42>p{UKsdf(W>aXh4XFWMN!vXQyg+d$xCasOkZB z>B*A76@_#(k(fAhG3v>LlaYf5y%-NB-umFdgOMYNC&R(_dKOlkN`LRwd#~#K)T^3L z#=n?M%*W#q0>9p$^Ou#8dx@g-$Gu5=J8EP%a@CwfD4`)EU(I{8P%U`0SS@<AR4r{r zSfDXjEiaO0(NEaWdW8Qd5gasDEngx{`68ilnz&7>!*3Frq^aA4rkHfgR~@0D^Q4k~ zgzhVnn_gtg!mO_|Yr%B#(v1Mfq;n7MNq)(>FOQtbfEh0#WD#-*c>p)SuCk_8jd?_) z%&O_e*Myfr_s@Za!%G(g<6_BRD=R`QonT_!GF#3QxgzDA313ATcHZ&*E~(Cb{~$<C z`6rXOk`Ez_0ZcnL{WD4Q8HR5GJ`wae-}`?$|Cdb~L7FO&yOKC+r5fq(fR(Won!Y0e zMcqJ&(9CnQ>>X*I^yF@~M>=6Uj4P=hY(q3~gwUK54NgtrR*gV^Bb#VBQ0E$XF#T%t z(0`2cXnYt7pZ621upYT9@h|(O@0T4kr=Ds4-Z_-#eZ9DyYZM!$PQ-IjYz%gy9#?J* zbz(c*a3@X+orE=F6Oe>x@s12ZjoJ}AW=HLW?Y9GVoR&Thb&__F4zA00W&W+DHeTqC zS$phIPwu3wy>^P0Z^<7gI%zxI9f#Zp773Nlk?nWMMPg0dBAtw#=}y`MJxL<KP4%FI zfL7>GUt%Ze@DXCE-F<e-laR(}V|s<y$&YXaR;Ua$@^J{R5$pzNipo8CXO6m=9e%Vs zyJI~AG>xh`I@0q&!^+T(qt5A&_1+#h<+E-`7lyg2Hy_C`<)Z*wg-d2zt#PI{O-uEZ zs@|NNo8u~I_3xt4j<G9k#y2%#alN^^6=+*4vxh1&pLR0J%(ffVg=n`JZ$nc;gUdSQ z1F#g>1d22bv02+-Ajz$Av)$^8b`@LMw@hKFy6|FP*L7h*09%=-QmTvEvcV__A0=^X zxf;H<bG35Z3$zk){cErZkAt44ollj8=;}+}@3&)>{mx^hSm<9ZQ8!!6^{q0CV@<f~ zsNqv3H@Ko37UMhyGFJhQmfoyee6M4KhsF+~A$sO%R7W}6FfHaK=H<#?alQ%Pyqx9Z zXrm%b15_0u*0dU56?_(Da{#Wt*0$EFG1RQHO$`gf_oE_!@eA(xLN{J2Zm?Ao;u_^I zJ2xYfmBS#fhWiJh>8AH0^ufE_i=ciLK5^gqH8PutNkK`GCgh+Lfe}M|@Fem;+Q##7 zgLOlPSEiEXXJE?l8E|C|r4jAr{;PBp<!>N(FV!$|Ud@gnw->>SHh~<L$IU`nwH7xm zvu+w1RIz2$Hoc<px6ue4^Lz-f;^*Ve!T4y|dzzlTPi_Pj)y-y;)veDQGyYf_b!v&i zi8WlP<2r>O2jT_{vqpvMw>IH4{ZxmuI)OHriyIJY%rL4E&V;_jw3fA534yzZiSo<} z8e!w}XIuWSif)LrI@7PRhmJ4#%QhzLDn?CMmJUa?@9FxjB3vB)^u`F4U2o~!>(Skt zQ;G1CXpe=2P+`5B5ljf(Y--TvEx1(PrYag8M|d4!9s$pT7YH;CmI@~=gx3&Mgy#$A zW&g8tBlZ3gKJgq|finaX0c+#NHLVVbh&mKU({?wgNPG&7ydD|>4wDr4C#p!Xl=6YH zCz9YOhqvTFdQ3XwTu3*hBhC-$lT&VrHfqgPbB-?0ZJ3l9!n;EJESUFGsv&p9DGaC| ya%4{<c)f&M?ncq)Fa}pS0$a5Wc2t6ym;i`D`}W6xM^k`Gf5<X4c0!8yl<dDe`tJPz delta 1751 zcmZ8iO>7fK6rSCzv-Ub(|HMw5*uf3~!lofo8;Q1%@)HzDIaDef028`#Jkw;IW!IY7 z6cVik6{zZ=6wruUr6x!{BGGbbFTM5FLnWk2-9vlkQmIl;J@vhH(ujJs-+uGv&71c( z<F6;595&}O8H2#@<^0(jw=0j$vhwTW$<Wr_Vxve2)!kB~6n}>rL-BXGF}!K8M7!J= zxk9|MM%d`O!M|4sJ~Z7JxkfzoDxm{3bDuOS7YH?J_CBFmrrhgmRH=TMSUG7b4_`c0 zG({VLz2!G*EmsD@2N8-0CHb3L85sro&&<M!rOsxs=6kbm+a2z=Su2qFK1(UdlYmvp z`hKt0Kr_+>6T<PmD9zbTKVbHnF9I-SYm-?t#VZKo08{dbHl4-oQt+OGSA3z$U$mbr zFUYm??L?TQxd%!}>_SM|h4$bhMa*ozLp2!bG%-hLUYd!ca=BlhEv_4n6h7U{Kred8 z%teg^y_v1u-0ZI{;MzMq)!2NgJ=9I@vf-`~vvPZ+o8D!k-2pn-%><Q@fJCRo2P(K$ z4O3w{G{Q`%g^6&0mcG)vX4p@M*40NUKOT&=4{X<hdZ_QH-E1%(X6f)f^`Y6#g}Lp6 zJ9kJoe}z!>Lvo2+CBei!(k+CA?L%RHM^Ol{!#hw>_#UC<o+LEs$Q%jAwkN}EEFtY` z`{>6cG{3|Zn5QyiO68ve@83?)QL65sD>%kEYnNBIr$RE?6YWmDK`T3b&_F7*M^#38 zFnbV+@<F6K!u3}j@0AKeegI%=_?o{_Z*o@m{Gc9B)*WwVW`@_~r^&i9C7&jzE0z+e z4&54Dgbt`XqD~#rTyYr%J(*7p9eI@nQ<mCD-C`@kZ>=-Vv4t!>(&DDUzT`Q%k~&w8 z(vA=t9mY2V<Eyjr@8xN0T0T#ehfAOrB}B{bu&8g91svT5PyR?<Ji{k&QfFSgbCj8H z3bt^>D1Q?bO@th(?X@g!!t*Gem3NK1<AeMV4$wJ11<*(ev1T{<s^HToJ0{1| z<*^y0u|+h-(fg>s(-h(s<!{Lw=}GH2h#Dz44vx2KJ3f!k!~UVT1k)$r6`#px>0>Yc zOn;}`K`)~Img_Jtu!?*UhWrf5R1U$4jTIgr+LI`sM<^gv5#lV&A;TQ>Jj)=%fTJQL z3Quz2w|v)z(sbPBW}F6o0hQwLOFB?X<A>xCvsQWC-N=ANE#LFtDtI85&A*ngGq{4r zHLxinjr+HRrScV&x6o3OzlYNa*Kbmo4>qA<8g*I$KZ`mT9Taknxo*SYOgLN2?gX2b z4h}raR;HIxDc<EjlmE+Mq;u9{&L`}-{5$*e0;V8Jx=j%T&P~Rxz4cpV=wH6qxT4N( zcN`u!W?W?Fhcq~AaJ-ZFNtwvk7USBZ^ZXRTX@oNfLjaf|<ZT43V6RnP&sQAckJ8Ow zU&7muW3{=B;2>ZEqKs{~z+2IR4A}O5Y?i_&Q7OJV$ADoZ0{=9U%0Mo8IXPjNJeH5i ziNZl;QJycfm3QU0h4Xp-KCX;izm9BO)&}bzxk%3=#Jz%R_qXdY+$>7Y-Sp{(%T8fk a1x7ib>^+5;<&<RaQB}oIOvUI+7XJn2yOs6; diff --git a/webssh/urls.py b/webssh/urls.py index b459082..4eb9422 100644 --- a/webssh/urls.py +++ b/webssh/urls.py @@ -2,9 +2,9 @@ from . import views -app_name = 'webssh' +app_name="webssh" urlpatterns = [ - path('lists/', views.lists, name='lists'), + path('hosts/', views.hosts, name='hosts'), path('terminal/', views.terminal, name='terminal'), path('logs/', views.logs, name='logs'), ] diff --git a/webssh/views.py b/webssh/views.py index 0eb27a6..0e7adfa 100644 --- a/webssh/views.py +++ b/webssh/views.py @@ -3,14 +3,20 @@ from .models import TerminalLog from util.tool import login_required, post_required, admin_required from django.http import JsonResponse +from django.db.models import Q from .forms import HostForm # Create your views here. @login_required -def lists(request): - hosts = RemoteUserBindHost.objects.all() - return render(request, 'webssh/host_lists.html', locals()) +def hosts(request): + if request.session['issuperuser']: + hosts = RemoteUserBindHost.objects.all() + else: + hosts = RemoteUserBindHost.objects.filter( + Q(user__username = request.session['username']) | Q(group__user__username = request.session['username']) + ).distinct() + return render(request, 'webssh/hosts.html', locals()) @login_required @@ -30,5 +36,5 @@ def terminal(request): @admin_required def logs(request): logs = TerminalLog.objects.all() - return render(request, 'webssh/terminal_logs.html', locals()) + return render(request, 'webssh/logs.html', locals()) diff --git a/webssh/websocket.py b/webssh/websocket.py index afd8264..01f1707 100644 --- a/webssh/websocket.py +++ b/webssh/websocket.py @@ -7,6 +7,7 @@ from devops.settings import TMP_DIR from server.models import RemoteUserBindHost from webssh.models import TerminalLog, TerminalLogDetail +from django.db.models import Q import os import json import re @@ -84,6 +85,17 @@ def connect(self): ssh_key_name = '123456' hostid = int(ssh_args.get('hostid')) try: + if not self.session['issuperuser']: # 普通用户判断是否有相关主机或者权限 + hosts = RemoteUserBindHost.objects.filter( + Q(id=hostid), + Q(user__username = self.session['username']) | Q(group__user__username = self.session['username']), + ).distinct() + if not hosts: + self.message['status'] = 2 + self.message['message'] = 'Host is not exist...' + message = json.dumps(self.message) + self.send(message) + self.close(3001) self.remote_host = RemoteUserBindHost.objects.get(id=hostid) if not self.remote_host.enabled: try: diff --git a/webtelnet/__pycache__/urls.cpython-37.pyc b/webtelnet/__pycache__/urls.cpython-37.pyc index 883a9b67d095e97776b63197fbad33677293f504..cbae6fe2652051a49cd220d1a3926449f52ad311 100644 GIT binary patch delta 22 ccmZ3(w1$b-iI<m)0SKn7caEL2kyoA(066XiEdT%j delta 22 ccmZ3(w1$b-iI<m)0SNka9AoEg<dtUx05qipr2qf` diff --git a/webtelnet/__pycache__/views.cpython-37.pyc b/webtelnet/__pycache__/views.cpython-37.pyc index 4c318c3ec9929e9c81da258a244328271174718d..cc7030190e4b5ab7c0668f4d3456ab2c9313aed1 100644 GIT binary patch delta 20 acmaFD_JobwiI<m)0SGSWIc?;QWCj2<(FC3V delta 20 acmaFD_JobwiI<m)0SLl$95-@DG6MiHY6Kww diff --git a/webtelnet/__pycache__/websocket.cpython-37.pyc b/webtelnet/__pycache__/websocket.cpython-37.pyc index 1ebe8bfaf38035d9ddf48a3a7819fd88e8aaa700..5e539d4d19b4d784800852d845b2378e09b380c6 100644 GIT binary patch delta 1757 zcmZ8h%WoS+7~k1=*X#8&e#CjWiR+Z6uBw0(s<vrqXi7=cLnRayS~t{nJ(Fae&90f* zB(&HHh#rt=QL7QRR!UAF!L27aaX<(OPH;hc<AUgcKLAdA-`F&yyPDsA^S$PK%|0oA zRY}b!lePxW!$0QV{c!YAYM6ZcXo}v5xtWc@+91)WMol+c%c^g#mb+s!qm{1}-qF0_ zh{lH2ZT^^O_)@!8xUPBnHI2q;;+|G3E@(7KQ};BQV&vn2+AuYjv}*cM_<>O+A$jZR zRT3fcraoR6gaCLEA&Zbh$jkfs*r8D%|2cVacKNDcTr9h6WkrbPH<(xt{I>i~KS8qc zFTF}ivN-U2X#%vvs9-`kz8A(h+wcQcU-Lx(?(&hTG`izs2!{ct<mJfA=>$*~tc$S3 znl3+ye7^K&M{AihIifuzA{C@tnccx4+lkQB0|H-^rAIWHIj|diK<2eQeK)tKb&ZaJ zmNWvoQEHsiXjc9ZId%+PwTzylj#kfuINvIO>ic(2|4%uG%Ck`7p@<ehi60Wa)hlwZ zz#zH&T=IRt{4bm(e>P$}`Bt$t+%?rP#nwnSs;JS{Sl8-PrEZMoyLND>qX8#M3lDS% zY`kN3tWLBO>)3SYvC)l#Y`itGqIKe*5rUfe)+hw7a|*P%Ye1v*J-xq2SsCdHn(ZF$ ztC#!V3N7vpz`W=%4a0KF+@7gG>92%QN4Wl~<2}=1%f|uk7O(r8<pyVE&kxE<Sa!VW z>1jS8%h579E-yq27sI$C#AciEO_&jwEZw0z1J*#MkX5fEdW{VRoWd&iH`_hdzCgYg z9oo5@TZBm|JEBY-(YWn03MV~FqbEI#ZL{0LZ>}@WamuPj7*TT-hYd%a&}uh?*!DRM zqk*%*{LNr>Mm@jIRVPEU(QLAI5Lzg^>w7F5@ceC_M@vIA{WeR41FJ0HSPMwcw&q7d zbH#B3I1z7XXhBqg<23_5DX&<i$_y$d5!CVjO*`hQ(&w+r`__e>1V5rYA{+;(#RKMg zET}j5s^BMp6B?L_;L0KT;+Rxus(dIviAK4;QPcN4)(q;@fjHD{+P_kZpm++F*aFZD zi`XPX<fYiqO9^5UiyYNWVA&+0FaxMR-=|3CsT;dNE@R_Dv*|j_3#wVZ09$?@;fxYc z*7Pak>W5K6wkp>wQj-WO2fWcdDF@@DC6&&bxJSpme+<^V8sSIeLVRNEU=E=TMegUg zBR`7&lEg9O1TCWcth|*d=I4-uaY5hKnCn&zaP?_otnxMrl>Q4&qntIFv&DAhkBOfO z7;$L14H4id@T#(sUna-1I9Sd%Q2eURUq|*C`CD>g5!w7K!a0Q35L8JDKw%=ts3V+4 zQ0fOG;Bz4OTF#|zF5`cXW2g94gqsK`4U_eH6M_;=NU2`m?`@UvDO6H_%|k#Pg(Ywz zEt1Gsi`HQ~!I57bk$<NuWI>kFOXQSnr(YQlGqlz4R{d#udwK)<<_a~+y<~35FVf{} x4Y1^w5L7$S(f-*z3QrELbKqdpW#_RM0;`;WLBcl+D+9|SmW~6JBKCll{}(17l)wN0 delta 1601 zcmZ8h&2QX96!$pxuGiV`-F$97HXG5zqD@=1w4tOxQw18Nl8O|OYzo$9XOh@;)?w^z zNZ3_C^jg$t;6_k-M&i;F2mS)o3y6ym2QHQ1fH<O_cyD$gDz@h5H*ened*g2^N9FW< zDrGA0{5pT_{l5kd(}U#O!zsEH^Rm?}RcOr1RdahL)7^Zv@Sf5b)D%{{Yx0Lg!Ivhg zg;k}Yt|-)`z4w)B-<t|e($sy0rWpCGr`k_r%ZimgeERX@4WbbZeF;Y$&-Mesa|n5a zf;du3LqkA5JHK${&dy%A<u@)|ws*MivU(_fR!@_hP<t#=5~q89BV!`14U>Yfw5beg z@nM8Az$syCXEVJ(8L+Oy3jQGCNc(ci4Kr@m)ms`(Js>T`&b1Ub=jMZFd$UvnI!^UD zg{H+X+Vm`>TGW)V0IB}nNwt$E@Zak29}@mXw+JW78B`W_EOC%?i*8?AmoAEKe>*0r z0e7$+?@~i;gJ#=_aJZ#N=eY+e1XXJ3t#~WeGFl0m|4MJ0a8`0hHk6k6IUy*Sa|hf! zcz+6X>{zD-sy<S?dpsLOx`JxQV_osl?suFPAN4@jROm^cNW^!^g!nXGI2eU0?GLQ_ z&2o*ia>EbHp1<id%1&cuW`>W!sq-CFG)jKhWPHyKLhdv+_w{DDac0(1xdj4|!5Vd+ z0zZmv`9bJVi$tnJ_X{^*S9XFjb%NTu$0(TGFaFC!Uu^9oCkTQ4dj<?yYLwVz>w#at z%eeGy>5+yeISvm`OJ_T^AlUUejpCuR&HQE<^*TY&++n;KFur*~K0m!?mBrV_;6MR& z^q}tVu&8H~g?s`njfp45yvi$LIx$i?i<~5abkNJ$wjVMZt#J&CpA#F2YX`l2QmP_g z+ST5Wc?}lYHNF|}7m+P3p%EEx6qBJ+ejcR@UFo{tXuw$8)TxJD&hN<(<H%lu6|4g& zx<O2mC22sDB*hsslgko=sGU7vnxr4s%r$a=QAV-4=dea-<@r2p`6~!$EQ(PE?O{BN zoDzbp!DXZ>2r_KEHnIv)5z2(OFZAoa2XO{FUTsg-hF?P=Srd7QvACAT$)uP|j+dTK zCNe>@n`|KNC4Zd7QR4(v;(Qi9k?#358bn&S2hGuBLFP~f<A<JYG0(GfJ}Ev)l`6OJ zKw9{pn>c56=G<dni0@NB^kHU^;nji=hl5W`Epe0{%l|jZmdURo`;vH)9>0NX84Z6K zsVfMwbVZ;bH`()5<i3O;#h;IWW6a0mV&;Q8WfZ|C@+E{71U!vWwq1vyf;yyR+b3<e z2tS2F@)tG&)C;fz9Cm{wv&L0p!c21HTLa?HOohygTy~jU6fAqL-HEyihtcr$cb9K3 z-L#h%mX{Zo-nJK37H_??YTsVGxxm*zfWM9)TaU(0{C6ny9vO4nK5csJRqRlJVoB0@ R^YF#NGgKTs7{?x?_z%f<YvKR^ diff --git a/webtelnet/urls.py b/webtelnet/urls.py index 58ebba5..18b99a0 100644 --- a/webtelnet/urls.py +++ b/webtelnet/urls.py @@ -2,7 +2,7 @@ from . import views -app_name = 'webtelnet' +app_name="webtelnet" urlpatterns = [ path('terminal/', views.terminal, name='terminal'), ] diff --git a/webtelnet/websocket.py b/webtelnet/websocket.py index 9c641db..c749116 100644 --- a/webtelnet/websocket.py +++ b/webtelnet/websocket.py @@ -5,6 +5,7 @@ import django.utils.timezone as timezone from server.models import RemoteUserBindHost from webssh.models import TerminalLog, TerminalLogDetail +from django.db.models import Q import json import time @@ -72,6 +73,17 @@ def connect(self): telnet_args = QueryDict(query_string=query_string, encoding='utf-8') hostid = int(telnet_args.get('hostid')) try: + if not self.session['issuperuser']: # 普通用户判断是否有相关主机或者权限 + hosts = RemoteUserBindHost.objects.filter( + Q(id=hostid), + Q(user__username = self.session['username']) | Q(group__user__username = self.session['username']), + ).distinct() + if not hosts: + self.message['status'] = 2 + self.message['message'] = 'Host is not exist...' + message = json.dumps(self.message) + self.send(message) + self.close(3001) self.remote_host = RemoteUserBindHost.objects.get(id=hostid) if not self.remote_host.enabled: try: