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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;用户</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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;主机列表</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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;批量命令 - 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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;批量命令日志 - 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>&nbsp;&nbsp;{% 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>&nbsp;&nbsp;{% 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>&nbsp;&nbsp;{% 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>&nbsp;&nbsp;<a href="{% url 'user:user_edit' user.id %}">修改</a>&nbsp;&nbsp;删除
+				  </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^&#X14>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: