├── .gitignore ├── LICENSE ├── ReadMe.rst ├── apps ├── __init__.py ├── account │ ├── __init__.py │ ├── docs.py │ ├── handlers.py │ └── urls.py ├── admin │ ├── __init__.py │ ├── decos.py │ ├── handlers.py │ └── urls.py ├── auth2 │ ├── __init__.py │ ├── decos.py │ ├── docs.py │ ├── handlers.py │ └── urls.py ├── base │ ├── __init__.py │ ├── handlers.py │ └── urls.py ├── dashboard │ ├── __init__.py │ ├── handlers.py │ └── urls.py ├── project │ ├── __init__.py │ ├── handlers.py │ └── urls.py ├── share │ ├── __init__.py │ ├── docs.py │ ├── handlers.py │ └── urls.py ├── testdata │ ├── __init__.py │ ├── docs.py │ ├── handlers.py │ └── urls.py ├── webapp │ ├── __init__.py │ ├── handlers.py │ └── urls.py └── ws │ ├── __init__.py │ ├── handlers.py │ └── urls.py ├── config.py ├── config_api.py ├── init_app.py ├── myapplication.py ├── nginx_config ├── test-api.conf └── test.conf ├── requirements.txt ├── route.py ├── service.sh ├── start.py └── static ├── favicon.ico └── templates └── code └── argument.codetpl /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # idea 104 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ReadMe.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | ReadMe 3 | ================= 4 | 5 | .. contents:: 目录 6 | 7 | 8 | 9 | 10 | 注意事项 11 | ================ 12 | 13 | - 依赖dtlib项目 14 | - python3.5以上 15 | - 首次部署时要使用init_app.py初始化 16 | - 依赖mongodb 17 | 18 | 根目录 19 | ============== 20 | 21 | 22 | .. csv-table:: 23 | :header: 目录名称,功能介绍 24 | 25 | apitests,接口测试代码 26 | apps,所有的web系统 27 | config,本系统运行所需要的配置目录 28 | lib,本系统调用的一些公共工具 29 | static,静态文件目录 30 | scripts,中间写的一些脚本 31 | 32 | 33 | 34 | 主要模块 35 | ============== 36 | 37 | 38 | 39 | .. csv-table:: 40 | :header: 模块名称,备注 41 | 42 | admin,后台运营和管理 43 | jenkins,持续集成相关功能 44 | source,企业的一些资源登记 45 | auth2.0,和扫码相关的服务 46 | ws,websocket 功能 47 | 48 | 发布日志 49 | =========== 50 | 51 | 4.18.5.18.1 52 | --------------- 53 | 54 | - 新增Tag功能,用于区分同一个项目不同执行环境 55 | - 修复一些bug 56 | 57 | 4.18.4.24.1 58 | --------------- 59 | 60 | - 去除对aiomotorengine的依赖,改用motor 1.2.1 61 | - 升级对Mongo3.6的支持,去除对Mongo3.2及以下版本的支持 62 | - 支持Python3.6 63 | 64 | 4.18.4.19.1 65 | --------------- 66 | 67 | - 新增电视展示功能 68 | - 默认开启跨域共享资源 69 | - 修复一些bug 70 | 71 | 3.17.5.29.1 72 | --------------- 73 | 74 | - 重大改变,所有和mongodb相关的时间全部采用utc时间来储存 75 | - 所有的同步和异步的装饰器进行了统一 76 | 77 | 3.17.5.4.2 78 | -------------------- 79 | 80 | - 创建默认的project,对于新注册用户 81 | - 新注册用户,现在默认是激活状态 82 | - 加入组织的用户,现在不用审核,直接激活 83 | 84 | 3.17.5.4.1 85 | -------------- 86 | 87 | - 增加了base_doc内容 88 | - 删除了一些不需要的内容 89 | 90 | 3.16.11.22.1 91 | -------------------- 92 | 93 | - 使用硬编码数据重构了代码 94 | - 添加测试项目,增加了mark项目描述字段 95 | - 添加了管理员账号对注册账号的统计功能 96 | - 手机风控数据收集接口 97 | - 修改了安全测试这一块的字段内容 98 | - 操作日志也加上组织的tag 99 | - 加了手机信息收集 100 | - 手机信息收集加了一些请求头信息 101 | - 加入了auth应用信息 102 | 103 | 3.16.11.16.5 104 | -------------------- 105 | 106 | - 完成手机授权功能 107 | 108 | 3.16.11.08.1 109 | ------------------------ 110 | 111 | - 加入了微信的移动端的认证 112 | 113 | 114 | 3.16.10.28.1 115 | -------------------- 116 | 117 | - 把token的相关表进行了更改 118 | - 增加了各种平台的交叉认证方式 119 | 120 | 121 | 3.16.10.25.1 122 | ------------------- 123 | 124 | - 增加了测试的应用模块,和token 125 | - 修复了wechat和user之间的关系表 126 | - 修复user无法获取组织的bug 127 | - 增加移动端的token的一节 128 | 129 | 130 | 3.16.10.11.1 131 | ----------------- 132 | 133 | - log_session需要完全删除掉的bug 134 | - 增加用户ID修改和昵称修改的接口 135 | 136 | 137 | 2.16.09.30.2 138 | ---------------------- 139 | 140 | - 完成了组织邀请码的机制 141 | - 头部加入了json描述和cookie值 142 | - ensure_ascii 解决json对中文的编码问题 143 | - 将一个数据放在一张表里面 144 | - 设置头部charset,Content-Type,Cookie中的token 145 | - 进行结果过滤,没显示details 146 | - 解决了一个总有多余的字段产生的,激活接口的bug 147 | - 接口调用统计,目前只统计全网的 148 | 149 | 2.16.09.19.11 150 | ------------------- 151 | 152 | - 用户注册时,会默认生成组织,组织关系,app等数据 153 | - 单独的decos,api,单独的session 154 | - 修复了一些bug 155 | - 可用的版本 156 | - 获取用户所有的组织信息 157 | - 启动了本地的redis session,因为内存的session有问题,本地成功,但是服务不能 158 | 159 | 160 | 161 | 2.16.09.07.2 162 | ------------------ 163 | 164 | - 修复token的bug 165 | - 修复登出的bug 166 | 167 | 2.16.09.07.1 168 | ---------------------- 169 | 170 | - 不能使用tornadsession来支持session 171 | - 因为使用token就表明是默认的 172 | 173 | 2.16.7.22.1 174 | ---------------- 175 | 176 | - 加入了torndsession来支持session 177 | - logsession是自己实现的一个登录的api-token体系 178 | 179 | 2.16.7.21.1 180 | ----------------- 181 | 182 | - 修复了线程池不为1的时候,异步redis的io的身份认证的bug 183 | - 加入了session存储到mongodb中 184 | - 结构进行了调整,增加了account的应用 185 | - 迁移了接口测试数据的保存脚本 186 | 187 | 2.16.5.24.2 188 | ---------------- 189 | 190 | - 解决了ioloop的替换的问题asyncio 191 | - 解决了aiomotorengine的集成问题 192 | - 迁移过来了微信扫码登录的功能 193 | - 完成了redis同步和异步io的demo程序 194 | 195 | 2.16.5.17.1 196 | ----------------------- 197 | 198 | - 完成了多核心的服务器的启动 199 | 200 | 201 | 2.16.5.12.1 202 | -------------------- 203 | 204 | - 新增py3项目 205 | 206 | 207 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/__init__.py -------------------------------------------------------------------------------- /apps/account/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/account/__init__.py -------------------------------------------------------------------------------- /apps/account/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | """ 4 | from aiomotorengine import BooleanField, StringField 5 | 6 | from dtlib.tornado.base_docs import UserBaseDocument 7 | 8 | 9 | class LanAppUser(UserBaseDocument): 10 | """ 11 | 内网应用的用户 12 | """ 13 | __collection__ = "lan_app_users" 14 | is_active = BooleanField() 15 | 16 | -------------------------------------------------------------------------------- /apps/account/handlers.py: -------------------------------------------------------------------------------- 1 | # import pymongo 2 | import motor 3 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 4 | from bson import ObjectId 5 | from dtlib import jsontool 6 | from dtlib.aio.decos import my_async_jsonp 7 | from dtlib.dtlog import dlog 8 | from dtlib.randtool import get_uuid1_key 9 | import hashlib 10 | # from dtlib.tornado.account_docs import Organization, UserOrgRelation, User 11 | from dtlib.tornado.decos import token_required 12 | # from dtlib.tornado.docs import UserDetailInfo, UserRegInfo, FeedBackMsg 13 | # from dtlib.tornado.status_cls import UserStatus, ResCode 14 | # from dtlib.tornado.ttl_docs import WebToken, MobileToken, PcAuthMobileToken, AccessToken, TtlOrgInviteCode 15 | from dtlib.utils import list_have_none_mem, get_rand_salt, hashlibmd5with_salt 16 | from dtlib.web.constcls import ConstData 17 | from dtlib.web.decos import deco_jsonp 18 | from dtlib.web.tools import get_std_json_response 19 | from dtlib.web.valuedict import ClientTypeDict 20 | 21 | # from apps.account.docs import LanAppUser 22 | from config import HTTP_PORT, mongodb_cfg, SERVER_PROCESS 23 | # from config_api import capt_image_domain 24 | # from xt_base.document.auth2_docs import MobileAuthPcToken 25 | # from dtlib.tornado.utils import user_id_is_legal, create_dft_organization, create_org_app, create_dft_org_rel 26 | import traceback 27 | 28 | from dtlib.tornado.utils import set_default_rc_tag 29 | from dtlib.tornado.status_cls import UserStatus, UserRegWay 30 | 31 | 32 | class CheckEnv(MyUserBaseHandler): 33 | """ 34 | 检查环境 35 | """ 36 | 37 | @my_async_jsonp 38 | async def get(self): 39 | tbl_out = { 40 | 'http_port': HTTP_PORT, 41 | 'process_num ': SERVER_PROCESS, 42 | 'db_server': mongodb_cfg.host, 43 | 'mongo_port': mongodb_cfg.port 44 | } 45 | try: 46 | con = motor.MotorClient(host=mongodb_cfg.host, port=mongodb_cfg.port) 47 | db = con[mongodb_cfg.db_name] 48 | res = await db.authenticate(mongodb_cfg.user_name, mongodb_cfg.user_pwd) 49 | if res is True: 50 | tbl_out['connect'] = True 51 | user_id = "admin" 52 | db = self.get_async_mongo() 53 | user_col = db.g_users 54 | res = await user_col.find_one({'user_id': user_id}) 55 | if res: 56 | return ConstData.msg_exist 57 | else: 58 | tbl_out['connect'] = False 59 | except: 60 | traceback.print_exc() 61 | tbl_out['connect'] = "mongodb connect error" 62 | 63 | return get_std_json_response(data=jsontool.dumps(tbl_out, ensure_ascii=False)) 64 | 65 | 66 | class AccountInit(MyUserBaseHandler): 67 | @my_async_jsonp 68 | async def get(self): 69 | """ 70 | 初始化账号 71 | :return: 72 | """ 73 | user_id = "admin" 74 | passwd = "admin@2018" 75 | u_name = 'your nickname' 76 | 77 | db = self.get_async_mongo() 78 | user_col = db.g_users 79 | user_reg_col = db.user_reg_info 80 | 81 | res = await user_col.find_one({'user_id': user_id}) 82 | if res: 83 | return ConstData.msg_exist 84 | rand_salt = get_rand_salt() 85 | new_user = { 86 | 'user_id': user_id, 87 | 'salt': rand_salt, 88 | 'nickname': u_name, 89 | 'passwd': hashlibmd5with_salt(passwd, rand_salt) 90 | } 91 | new_user = set_default_rc_tag(new_user) 92 | new_user.update(self.set_template()) 93 | user_res = await user_col.insert(new_user) 94 | # new_user = await new_user.save() 95 | # """:type:User""" 96 | 97 | new_user_reg_info = { 98 | 'user': user_res, 99 | 'u_name': u_name 100 | } 101 | 102 | new_user_reg_info.update(self.set_http_tag()) 103 | new_user_reg_info = set_default_rc_tag(new_user_reg_info) 104 | await user_reg_col.insert(new_user_reg_info) 105 | # await user_reg_info.save() 106 | 107 | org = await self.create_dft_organization(new_user_reg_info, is_default=True) 108 | await self.create_dft_org_rel(new_user_reg_info, org, is_default=False, is_current=True) 109 | res_dict = await self.create_org_app(org) 110 | res_dict['user'] = new_user['user_id'] 111 | res_dict['password'] = passwd 112 | invite_json = jsontool.dumps(res_dict, ensure_ascii=False) 113 | return get_std_json_response(data=invite_json) 114 | 115 | @staticmethod 116 | def set_template(): 117 | return dict( 118 | status=UserStatus.init, 119 | active=True, 120 | reg_way=UserRegWay.web 121 | ) 122 | 123 | async def create_dft_organization(self, user, is_default=False): 124 | """ 125 | 创建默认的组织 126 | :type user: User 127 | :parameter is_default:是否是模板组织 128 | :return: 129 | """ 130 | # 为新用户建立默认的小组 131 | db = self.get_async_mongo() 132 | org_col = db.organization 133 | # default_org = Organization() 134 | 135 | new_org = dict( 136 | is_default=is_default, 137 | owner=user['user'], 138 | name = 'group template', # 默认组织名称和用户昵称一样,后面提供修改接口 139 | home_page = 'http://www.my-org-page.org' 140 | ) 141 | new_org = set_default_rc_tag(new_org) 142 | # default_org.owner_name = user.nickname # 冗余 143 | org_id = await org_col.insert(new_org) 144 | new_org['_id'] = org_id 145 | return new_org 146 | 147 | async def create_dft_org_rel(self, user, org, 148 | is_default=False, 149 | is_current=False, 150 | is_activate=True, 151 | is_owner=True 152 | ): 153 | """ 154 | 为组织和用户建立关联 155 | :param user: 156 | :type user:User 157 | :type org:Organization 158 | :param org: 159 | :return: 160 | """ 161 | # 建立它们的默认关系 162 | db = self.get_async_mongo() 163 | user_org_rel_col = db.user_org_rel 164 | 165 | new_rel = dict( 166 | organization=org['_id'], 167 | user=user['user'], 168 | is_default=is_default, 169 | is_current=is_current, 170 | is_owner=is_owner, 171 | is_active=is_activate 172 | ) 173 | new_rel = set_default_rc_tag(new_rel) 174 | # user_org_rel.user_name = user.nickname # 冗余 175 | return await user_org_rel_col.insert(new_rel) 176 | 177 | async def create_org_app(self, default_org): 178 | """ 179 | 为组织创建应用app 180 | :param default_org: 181 | :type org:Organization 182 | :return: 183 | """ 184 | 185 | # default_test_app = TestDataApp() 186 | db = self.get_async_mongo() 187 | app_col = db.test_data_app 188 | new_app = dict( 189 | app_id=get_uuid1_key(), 190 | app_key=hashlib.md5(get_uuid1_key().encode(encoding='utf-8')).hexdigest(), 191 | organization=default_org['_id'], 192 | o_name=default_org['name'], 193 | is_default=True # 是默认设置的 194 | ) 195 | 196 | new_app = set_default_rc_tag(new_app) 197 | app_id = await app_col.insert(new_app) 198 | new_app['_id'] = app_id 199 | return new_app 200 | 201 | 202 | class GetAuthUserOrganization(MyUserBaseHandler): 203 | @token_required() 204 | @my_async_jsonp 205 | async def get(self): 206 | """ 207 | 用户当前视图下的组织信息 208 | :return: 209 | """ 210 | res = await self.get_organization() 211 | db = self.get_async_mongo() 212 | org_col = db.organization 213 | org_res = await org_col.find_one({'_id': ObjectId(res)}) 214 | return get_std_json_response(data=jsontool.dumps(org_res, ensure_ascii=False)) 215 | 216 | 217 | class UserLogin(MyUserBaseHandler): 218 | """ 219 | 用户登录,目前感觉还没有用到 2016-10-25 220 | todo: 后面在启用的时候,还需要继续fixbug 221 | """ 222 | 223 | @deco_jsonp() 224 | async def post(self): 225 | 226 | post_dict = self.get_post_body_dict() 227 | user_id = post_dict.get(ConstData.user_form, None) 228 | passwd = post_dict.get(ConstData.passwd_form, None) 229 | 230 | if list_have_none_mem(*[user_id, passwd]): 231 | return ConstData.msg_args_wrong 232 | 233 | db = self.get_async_mongo() 234 | user_res = await db['g_users'].find_one({'user_id': user_id, 'is_del': False}, 235 | {'_id': 1, 'passwd': 1, 'salt': 1, 'is_lock': 1}) 236 | if not user_res: 237 | return ConstData.msg_fail 238 | 239 | try: 240 | if user_res['is_lock'] is True: 241 | return ConstData.msg_forbidden 242 | except: 243 | pass 244 | 245 | user_pass = user_res['passwd'] 246 | user_salt = user_res['salt'] 247 | _id = user_res['_id'] 248 | 249 | md5_password = hashlibmd5with_salt(passwd, user_salt) 250 | 251 | # auth_res = await User.auth(user_id, passwd) 252 | if md5_password == user_pass: 253 | # if auth_res: 254 | token_res = await self.create_token_session(_id, client_type=ClientTypeDict.browser) 255 | data = { 256 | "token": token_res['token'] 257 | } 258 | return get_std_json_response(msg="success", data=jsontool.dumps(data)) 259 | else: 260 | return ConstData.msg_fail 261 | 262 | 263 | class UserLogout(MyUserBaseHandler): 264 | """ 265 | 用户登录出,根据session来判断的,api,user,wechat都可以用这个 266 | """ 267 | 268 | @token_required() 269 | @my_async_jsonp 270 | async def get(self): 271 | # 记录日志 272 | 273 | await self.log_out() 274 | 275 | return ConstData.msg_succeed 276 | 277 | 278 | class SetAuthUserInfo(MyUserBaseHandler): 279 | """ 280 | 设置当前登录用户的信息,昵称,可以反复修改 281 | """ 282 | 283 | @token_required() 284 | @my_async_jsonp 285 | async def post(self): 286 | req_dict = self.get_post_body_dict() 287 | # passwd = req_dict.get('passwd', None) 288 | nickname = req_dict.get('nick_name', None) 289 | 290 | if list_have_none_mem(*[nickname]): 291 | return ConstData.msg_args_wrong 292 | 293 | db = self.get_async_mongo() 294 | user_col = db.g_users 295 | 296 | current_auth_user = await user_col.find_one({'_id': self.cache_session['user']}) 297 | 298 | current_auth_user['nickname'] = nickname 299 | await user_col.update({'_id': self.cache_session['user']}, 300 | {'$set': {'nickname': nickname}}, upsert=False) 301 | # current_auth_user = await current_auth_user.save() 302 | 303 | res_dict = dict( 304 | user_id=current_auth_user['user_id'], 305 | nickname=current_auth_user['nickname'], 306 | rc_time=current_auth_user['rc_time'] 307 | ) 308 | 309 | return get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 310 | 311 | 312 | class GetAuthUserInfo(MyUserBaseHandler): 313 | """ 314 | 获取当前登录用户的信息 315 | """ 316 | 317 | @token_required() 318 | @my_async_jsonp 319 | async def get(self): 320 | current_user_id = self.cache_session['user'] 321 | # """:type:User""" 322 | 323 | mongo_conn = self.get_async_mongo() 324 | 325 | user_col = mongo_conn['g_users'] 326 | 327 | current_auth_user = await user_col.find_one({'_id': ObjectId(current_user_id)}) 328 | # print("user_col.find_one({'user_id': %s})" % current_user_id) 329 | 330 | if not current_auth_user: 331 | return ConstData.msg_fail 332 | 333 | res_dict = dict( 334 | user_id=current_auth_user["user_id"], 335 | nickname=current_auth_user["nickname"], 336 | rc_time=current_auth_user["rc_time"], 337 | # status=current_auth_user["status"], 338 | ) 339 | 340 | res_data = jsontool.dumps(res_dict, ensure_ascii=False) 341 | dlog.debug(res_data) 342 | 343 | return get_std_json_response(data=res_data) 344 | 345 | 346 | class UpdateUserDetailInfo(MyUserBaseHandler): 347 | @token_required() 348 | @my_async_jsonp 349 | async def post(self): 350 | """ 351 | 创建用户的详细信息,联系信息 352 | :return: 353 | """ 354 | 355 | post_body_dict = self.get_post_body_dict() 356 | 357 | qq = post_body_dict.get('qq', None) 358 | email = post_body_dict.get('email', None) 359 | phone = post_body_dict.get('phone', None) 360 | 361 | db = self.get_async_mongo() 362 | user_detail_col = db.user_detail_info 363 | 364 | user = self.get_current_session_user() 365 | user_detail_res = await user_detail_col.find_one({'user': user}) 366 | user_detail = dict( 367 | qq=qq, 368 | email=email, 369 | phone=phone 370 | ) 371 | if user_detail_res is None: # 如果为空则创建一个 372 | user_col = db.g_users 373 | user_res = await user_col.find_one({'_id': user}) 374 | new_user_detail = dict( 375 | user=user, 376 | u_name=user_res['nickname'] 377 | ) 378 | new_user_detail = set_default_rc_tag(new_user_detail) 379 | user_detail.update(new_user_detail) 380 | 381 | user_detail_col.update({'user': user}, {'$set': user_detail}, upsert=True) 382 | return ConstData.msg_succeed 383 | 384 | 385 | class GetUserDetailInfo(MyUserBaseHandler): 386 | @token_required() 387 | @my_async_jsonp 388 | async def get(self): 389 | """ 390 | 获取用户的联系信息 391 | :return: 392 | """ 393 | 394 | db = self.get_async_mongo() 395 | user_col = db.user_detail_info 396 | user_detail = await user_col.find_one({'user': self.get_current_session_user()}) 397 | 398 | if user_detail is None: # 如果为空则创建一个 399 | user_detail = {} 400 | return get_std_json_response(data=jsontool.dumps(user_detail)) 401 | 402 | # class GetAuthUserAllOrganizations(MyUserBaseHandler): 403 | # # TODO: change to motor 404 | # @token_required() 405 | # @my_async_jsonp 406 | # async def get(self): 407 | # """ 408 | # 本用户的所有的组织信息 409 | # :return: 410 | # """ 411 | # # user_id = self.get_user_id() 412 | # current_user = self.get_current_session_user() 413 | # 414 | # if current_user is None: 415 | # return ConstData.msg_none 416 | # 417 | # # 获取所有的组织关系 418 | # org_rels = await UserOrgRelation.objects.filter(user=current_user).find_all() 419 | # """:type:list[UserOrgRelation]""" 420 | # 421 | # org_dict_list = [] 422 | # 423 | # for item in org_rels: 424 | # org = item.organization 425 | # """:type:Organization""" 426 | # org_dict = org.to_dict() 427 | # org_dict['is_current'] = item.is_current 428 | # org_dict_list.append(org_dict) 429 | # 430 | # # res = await self.get_organization() 431 | # return get_std_json_response(data=jsontool.dumps(org_dict_list, ensure_ascii=False)) 432 | 433 | # region Organization CRUBL 434 | 435 | # class CreateDefaultOrganization(MyUserBaseHandler): 436 | # """ 437 | # 创建默认的模板Organization 438 | # """ 439 | # 440 | # @token_required() 441 | # @my_async_jsonp 442 | # async def get(self): 443 | # user = self.get_current_session_user() 444 | # org = await create_dft_organization(user) 445 | # await create_dft_org_rel(user, org, is_default=False, is_current=True) 446 | # res = await create_org_app(org) 447 | # invite_json = jsontool.dumps(res.to_dict(), ensure_ascii=False) 448 | # return get_std_json_response(data=invite_json) 449 | # 450 | # 451 | # class CreateOrganization(MyUserBaseHandler): 452 | # # TODO: change to motor 453 | # """ 454 | # 创建Organization 455 | # """ 456 | # 457 | # async def post(self): 458 | # # 表单提取 459 | # 460 | # name = self.get_argument("name", None) 461 | # home_page = self.get_argument("home_page", None) 462 | # # owner = self.get_argument("owner", None) 463 | # 464 | # # 空值检查 465 | # if list_have_none_mem(*[name, home_page, 466 | # # owner, 467 | # ]): 468 | # return ConstData.msg_args_wrong 469 | # 470 | # # 数据表赋值 471 | # doc_obj = Organization() 472 | # 473 | # doc_obj.name = name 474 | # doc_obj.home_page = home_page 475 | # doc_obj.owner_id = self.cache_session.user_id 476 | # doc_obj.set_default_rc_tag() 477 | # await doc_obj.save() 478 | # 479 | # return ConstData.msg_succeed 480 | # 481 | # 482 | # class GetInviteCode(MyUserBaseHandler): 483 | # # TODO: change to motor 484 | # @token_required() 485 | # @my_async_jsonp 486 | # async def get(self): 487 | # """ 488 | # 生成邀请码 489 | # 1. 查询ttl状态码 490 | # 2. 如果没有则生成一条 491 | # 3. 如果有则用原来的 492 | # #. 生成一条邀请链接 493 | # :return: 494 | # """ 495 | # # 表单提取 496 | # 497 | # org_id = self.get_argument("org_id", None) 498 | # 499 | # # 空值检查 500 | # if list_have_none_mem(*[org_id, 501 | # ]): 502 | # return ConstData.msg_args_wrong 503 | # 504 | # # 数据表赋值 505 | # org = await Organization.objects.get(id=ObjectId(str(org_id))) 506 | # """:type:Organization""" 507 | # invite_obj = await TtlOrgInviteCode.objects.get(organization=org) 508 | # """:type:TtlOrgInviteCode""" 509 | # if invite_obj is None: 510 | # new_invite = TtlOrgInviteCode() 511 | # new_invite.invite_code = get_uuid1_key() # 生成一个邀请码 512 | # new_invite.organization = org 513 | # new_invite.o_name = org.name 514 | # new_invite.set_default_rc_tag() 515 | # await new_invite.save() 516 | # invite_json = jsontool.dumps(new_invite.to_dict(), ensure_ascii=False) 517 | # return get_std_json_response(data=invite_json) 518 | # 519 | # invite_json = jsontool.dumps(invite_obj.to_dict(), ensure_ascii=False) 520 | # return get_std_json_response(data=invite_json) 521 | # 522 | # 523 | # class GetOrgInviteLink(MyUserBaseHandler): 524 | # # TODO: change to motor 525 | # @token_required() 526 | # @my_async_jsonp 527 | # async def get(self): 528 | # """ 529 | # 生成邀请码 530 | # 1. 查询ttl状态码 531 | # 2. 如果没有则生成一条 532 | # 3. 如果有则用原来的 533 | # #. 生成一条邀请链接 534 | # :return: 535 | # """ 536 | # # 表单提取 537 | # 538 | # # org_id = self.get_argument("org_id", None) 539 | # 540 | # # 空值检查 541 | # # if list_have_none_mem(*[org_id, 542 | # # ]): 543 | # # return ConstData.msg_args_wrong 544 | # 545 | # # 数据表赋值 546 | # org = await self.get_organization() 547 | # """:type:Organization""" 548 | # 549 | # # org = await Organization.objects.get(id=ObjectId(str(org_id))) 550 | # 551 | # invite_obj = await TtlOrgInviteCode.objects.get(organization=org) 552 | # """:type:TtlOrgInviteCode""" 553 | # if invite_obj is None: 554 | # new_invite = TtlOrgInviteCode() 555 | # new_invite.invite_code = get_uuid1_key() # 生成一个邀请码 556 | # new_invite.organization = org 557 | # new_invite.o_name = org.name 558 | # new_invite.set_default_rc_tag() 559 | # invite_obj = await new_invite.save() 560 | # 561 | # invite_link = 'http://api.apiapp.cc/account/accept-org-invite-by-link/?invite_code=%s' \ 562 | # % invite_obj.invite_code 563 | # 564 | # invite_dict = invite_obj.to_dict() 565 | # invite_dict['link'] = invite_link 566 | # 567 | # invite_json = jsontool.dumps(invite_dict, ensure_ascii=False) 568 | # return get_std_json_response(data=invite_json) 569 | # 570 | # 571 | # class AcceptOrgInviteByLink(MyUserBaseHandler): 572 | # # TODO: change to motor 573 | # @my_async_jsonp 574 | # async def get(self): 575 | # """ 576 | # 对于有cookie的用户,加入组织 577 | # :return: 578 | # """ 579 | # 580 | # log_session = await self.set_session_by_token(cookie_enable=True) 581 | # 582 | # if log_session is None: 583 | # return ConstData.msg_unauthorized 584 | # 585 | # # todo 后续完成一个新接口,对于有cookie的用户,可以直接通过打开链接的方式来加入到组织, 586 | # # 如果用户没有注册的话,就先让注册,会涉及专门的注册增长线路 587 | # invite_code = self.get_argument('invite_code', None) 588 | # 589 | # if list_have_none_mem(*[invite_code, ]): 590 | # return ConstData.msg_args_wrong 591 | # 592 | # invite_code_obj = await TtlOrgInviteCode.objects.get(invite_code=str(invite_code)) 593 | # """":type:TtlOrgInviteCode""" 594 | # 595 | # if invite_code_obj is None: 596 | # return ConstData.msg_none 597 | # 598 | # # 根据要求建立此用户和组织之间的关系 599 | # current_user = self.get_current_session_user() 600 | # """:type:User""" 601 | # 602 | # org = invite_code_obj.organization 603 | # """:type:Organization""" 604 | # 605 | # # 如果已经有关系,就不需要建立关系了 606 | # new_user_org_rel = await UserOrgRelation.objects.get(user=current_user, organization=org) 607 | # """:type:UserOrgRelation""" 608 | # 609 | # if new_user_org_rel is None: 610 | # # 新建组织关系 611 | # new_user_org_rel = UserOrgRelation() 612 | # new_user_org_rel.user = current_user 613 | # new_user_org_rel.user_name = current_user.nickname 614 | # new_user_org_rel.organization = org 615 | # new_user_org_rel.org_name = org.name 616 | # new_user_org_rel.is_current = False 617 | # new_user_org_rel.is_default = False 618 | # new_user_org_rel.is_owner = False 619 | # new_user_org_rel.is_active = True # 未被审核的时候,就没被激活,早期为了进入系统便捷,便不需要审核 620 | # new_user_org_rel.set_default_rc_tag() 621 | # new_user_org_rel = await new_user_org_rel.save() 622 | # """:type:UserOrgRelation""" 623 | # 624 | # # 到此处,可以保证有一个组织关系了,不管是旧的已经存在的还是新的 625 | # 626 | # if new_user_org_rel.is_current: # 如果这个组织就是当前组织 627 | # msg = '当前用户就在此邀请组织里面了' 628 | # return get_std_json_response(code=ResCode.ok, msg=msg) 629 | # 630 | # # 如果用户不在邀请组织里面 631 | # new_user_org_rel.is_current = True 632 | # 633 | # # 需要先将旧的is_current的组织状态去除 634 | # old_current_rels = await UserOrgRelation.objects.filter(user=current_user, is_current=True).find_all() 635 | # """:type:list[UserOrgRelation]""" 636 | # if len(old_current_rels) != 1: 637 | # msg = 'more than one current data,please contact the admin' 638 | # return get_std_json_response(code=403, msg=msg) 639 | # 640 | # old_current_rel = old_current_rels[0] 641 | # old_current_rel.is_current = False 642 | # 643 | # await new_user_org_rel.save() 644 | # await old_current_rel.save() 645 | # 646 | # return ConstData.msg_succeed 647 | # 648 | # 649 | # class ExitCurrentOrg(MyUserBaseHandler): 650 | # # TODO: change to motor 651 | # """ 652 | # 退出当前组织: 653 | # 1. 删除自己和当前组织的关系(is_del置位) 654 | # 2. 设置自己的当前组织为自己默认创建的组织 655 | # """ 656 | # 657 | # @token_required() 658 | # @my_async_jsonp 659 | # async def get(self): 660 | # current_user = self.get_current_session_user() 661 | # current_org = await self.get_organization() 662 | # """:type:Organization""" 663 | # 664 | # user_org_rel = await UserOrgRelation.objects.get( 665 | # user=current_user, 666 | # organization=current_org, 667 | # is_current=True) 668 | # """:type:UserOrgRelation""" 669 | # 670 | # assert user_org_rel is not None, '当前用户的组织信息不存在' 671 | # 672 | # if user_org_rel.is_owner: 673 | # # 如果當前组织就是原始创建的组织,则不允许退出 674 | # msg = 'default org , can not exit' 675 | # return get_std_json_response(code=ResCode.forbidden, data={}, msg=msg) 676 | # 677 | # user_org_rel.is_current = False # 切换组织 678 | # user_org_rel.is_del = True # 删除关系 679 | # 680 | # dft_rel = await UserOrgRelation.objects.get(user=current_user, is_owner=True) 681 | # """:type:UserOrgRelation""" 682 | # 683 | # dft_rel.is_current = True 684 | # dft_rel = await dft_rel.save() 685 | # await user_org_rel.save() 686 | # 687 | # if dft_rel is None: 688 | # return ConstData.msg_fail 689 | # 690 | # return ConstData.msg_succeed 691 | # 692 | # 693 | # class InviteUserToOrg(MyUserBaseHandler): 694 | # # TODO: change to motor 695 | # """ 696 | # 邀请用户到Organization 697 | # """ 698 | # 699 | # @token_required() 700 | # @my_async_jsonp 701 | # async def get(self): 702 | # """ 703 | # 1. 根据传入的串和当前打开此串的用户token来建立关系 704 | # 对于无cookie的用户可以使用此功能 705 | # :return: 706 | # """ 707 | # 708 | # # todo 后续完成一个新接口,对于有cookie的用户,可以直接通过打开链接的方式来加入到组织,如果用户没有注册的话,就先让注册,会涉及专门的注册增长线路 709 | # invite_code = self.get_argument('invite_code', None) 710 | # 711 | # if list_have_none_mem(*[invite_code, ]): 712 | # return ConstData.msg_args_wrong 713 | # 714 | # invite_code_obj = await TtlOrgInviteCode.objects.get(invite_code=str(invite_code)) 715 | # """":type:TtlOrgInviteCode""" 716 | # 717 | # if invite_code_obj is None: 718 | # return ConstData.msg_none 719 | # 720 | # # 根据要求建立此用户和组织之间的关系 721 | # current_user = self.get_current_session_user() 722 | # """:type:User""" 723 | # org = invite_code_obj.organization 724 | # """:type:Organization""" 725 | # 726 | # # 如果已经有关系,就不需要建立关系了 727 | # new_user_org_rel = await UserOrgRelation.objects.get(user=current_user, organization=org) 728 | # if new_user_org_rel is not None: 729 | # return ConstData.msg_exist 730 | # 731 | # # 如果不存在,则建立关系。需要先将旧的is_current的组织状态去除 732 | # old_current_rels = await UserOrgRelation.objects.filter(user=current_user, is_current=True).find_all() 733 | # """:type:list[UserOrgRelation]""" 734 | # if len(old_current_rels) != 1: 735 | # msg = 'more than one current data,please contact the admin' 736 | # return get_std_json_response(code=403, msg=msg) 737 | # 738 | # old_current_rel = old_current_rels[0] 739 | # old_current_rel.is_current = False 740 | # 741 | # # 新建组织关系 742 | # new_user_org_rel = UserOrgRelation() 743 | # new_user_org_rel.user = current_user 744 | # new_user_org_rel.user_name = current_user.nickname 745 | # new_user_org_rel.organization = org 746 | # new_user_org_rel.org_name = org.name 747 | # new_user_org_rel.is_current = True 748 | # new_user_org_rel.is_default = False 749 | # new_user_org_rel.is_owner = False 750 | # new_user_org_rel.is_active = True # 未被审核的时候,就没被激活,早期为了进入系统便捷,便不需要审核 751 | # new_user_org_rel.set_default_rc_tag() 752 | # 753 | # await old_current_rel.save() 754 | # await new_user_org_rel.save() 755 | # 756 | # return ConstData.msg_succeed 757 | # 758 | # 759 | # class ReadOrganization(MyUserBaseHandler): 760 | # # TODO: change to motor 761 | # """ 762 | # 读取Organization 763 | # """ 764 | # 765 | # async def get(self): 766 | # callback = self.get_argument('callback', None) 767 | # res = await Organization.objects.filter().find_all() 768 | # result = [] 769 | # for item in res: 770 | # result.append(item.to_dict()) 771 | # data = get_std_json_response(code=200, data=jsontool.dumps(result)) 772 | # data = '%s(%s)' % (callback, data) 773 | # self.write(data) 774 | # 775 | # 776 | # class UpdateOrganization(MyUserBaseHandler): 777 | # # TODO: change to motor 778 | # """ 779 | # 更改Organization名称 780 | # """ 781 | # 782 | # @token_required() 783 | # @deco_jsonp() 784 | # async def post(self): 785 | # post_dict = self.get_post_body_dict() 786 | # id = post_dict.get('id', None) 787 | # name = post_dict.get('name', None) 788 | # 789 | # if list_have_none_mem(*[id, name]): 790 | # return ConstData.msg_args_wrong 791 | # 792 | # update_org = await Organization.objects.get(id=ObjectId(str(id))) 793 | # 794 | # current_user = self.get_current_session_user() 795 | # if update_org.owner != current_user: 796 | # return ConstData.msg_forbidden 797 | # 798 | # update_org.name = name 799 | # await update_org.save() 800 | # res_dict = dict( 801 | # data=update_org 802 | # ) 803 | # 804 | # return get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 805 | # 806 | # 807 | # class DeleteOrganization(MyUserBaseHandler): 808 | # """ 809 | # 创建Organization 810 | # """ 811 | # 812 | # def get(self): 813 | # self.write('get') 814 | # 815 | # def post(self): 816 | # self.write('post') 817 | # 818 | # 819 | # class GetOrgMember(MyUserBaseHandler): 820 | # # TODO: change to motor 821 | # """ 822 | # 获取Organization中所有的成员,并显示激活状态 823 | # """ 824 | # 825 | # @token_required() 826 | # @deco_jsonp() 827 | # async def get(self): 828 | # org_id = self.get_argument('org_id', None) 829 | # 830 | # if list_have_none_mem(*[org_id]): 831 | # return ConstData.msg_args_wrong 832 | # 833 | # # cur_org = await Organization.objects.get(id=ObjectId(str(org_id))) 834 | # curr_org = await Organization.get_by_id(ObjectId(str(org_id))) 835 | # if curr_org is None: 836 | # return ConstData.msg_none 837 | # 838 | # current_rels = await UserOrgRelation.objects.filter(organization=curr_org).find_all() 839 | # """:type:list[UserOrgRelation]""" 840 | # 841 | # if len(current_rels) == 0: 842 | # return ConstData.msg_none 843 | # 844 | # user_list = [] 845 | # """:type:list[dict]""" 846 | # 847 | # for current_rel in current_rels: 848 | # user = current_rel.user 849 | # """:type:User""" 850 | # user_dict = dict( 851 | # user_id=user.user_id, 852 | # name=user.nickname, 853 | # is_active=current_rel.is_active 854 | # ) 855 | # user_list.append(user_dict) 856 | # 857 | # return get_std_json_response(data=jsontool.dumps(user_list, ensure_ascii=False)) 858 | # 859 | # 860 | # class AuditOrgMember(MyUserBaseHandler): 861 | # # TODO: change to motor 862 | # """ 863 | # 对用户加入组织的申请进行激活操作:确立有效的关系 864 | # """ 865 | # 866 | # @token_required() 867 | # @my_async_jsonp 868 | # async def get(self): 869 | # audit = self.get_argument('audit', None) # 审核结果 870 | # rel_id = self.get_argument('rel_id', None) # 需要审核的关系ID信息 871 | # 872 | # if list_have_none_mem(*[audit, rel_id]): 873 | # return ConstData.msg_args_wrong 874 | # 875 | # audit = True if audit == '1' else False 876 | # 877 | # curr_org = await self.get_organization() 878 | # if curr_org is None: 879 | # return ConstData.msg_none 880 | # 881 | # current_rels = await UserOrgRelation.objects.filter( 882 | # organization=curr_org, 883 | # id=ObjectId(str(rel_id)) 884 | # ).find_all() 885 | # """:type:list[UserOrgRelation]""" 886 | # 887 | # if len(current_rels) != 1: 888 | # return ConstData.msg_args_wrong 889 | # 890 | # current_rel = current_rels[0] 891 | # current_rel.is_active = audit 892 | # await current_rel.save() 893 | # 894 | # return ConstData.msg_succeed 895 | 896 | 897 | # endregion 898 | 899 | 900 | # code trash (2018-04-23) 901 | 902 | # class CreateUser(MyUserBaseHandler): 903 | # # TODO: change to motor 904 | # """ 905 | # 注册用户 906 | # 已启用管理员创建用户模式(2018-04-23) 907 | # """ 908 | # 909 | # @deco_jsonp() 910 | # async def post(self): 911 | # 912 | # post_dict = self.get_post_body_dict() 913 | # user_id = post_dict.get(ConstData.user_form, None) 914 | # passwd = post_dict.get(ConstData.passwd_form, None) 915 | # u_name = post_dict.get('u_name', None) 916 | # 917 | # if list_have_none_mem(*[user_id, passwd]): 918 | # return ConstData.msg_args_wrong 919 | # 920 | # if u_name is None: 921 | # u_name = 'your nickname' 922 | # 923 | # res = await User.is_exist(user_id) 924 | # if res is False: 925 | # return ConstData.msg_exist 926 | # 927 | # rand_salt = get_rand_salt() 928 | # new_user = User() 929 | # new_user.user_id = user_id 930 | # new_user.salt = rand_salt 931 | # new_user.nickname = u_name 932 | # new_user.passwd = hashlibmd5with_salt(passwd, rand_salt) 933 | # new_user.set_default_rc_tag() 934 | # new_user.set_template() 935 | # new_user = await new_user.save() 936 | # """:type:User""" 937 | # 938 | # user_reg_info = UserRegInfo() 939 | # user_reg_info.user = new_user 940 | # user_reg_info.u_name = new_user.nickname 941 | # user_reg_info.set_http_tag(http_req=self) 942 | # user_reg_info.set_default_rc_tag() 943 | # await user_reg_info.save() 944 | # return ConstData.msg_succeed 945 | # 946 | # class SetAuthUserPwd(MyUserBaseHandler): 947 | # # TODO: change to motor 948 | # """ 949 | # 设置当前登录用户的ID,只能修改一次 950 | # """ 951 | # 952 | # @token_required() 953 | # @my_async_jsonp 954 | # async def post(self): 955 | # req_dict = self.get_post_body_dict() 956 | # new_user_pwd = req_dict.get('new_pwd', None) 957 | # old_user_id = req_dict.get('old_pwd', None) 958 | # 959 | # if list_have_none_mem(*[old_user_id, new_user_pwd]): 960 | # return ConstData.msg_args_wrong 961 | # 962 | # # 做一些密码复杂性和合法性检查 963 | # if not user_id_is_legal(old_user_id): 964 | # return ConstData.msg_args_wrong 965 | # 966 | # # 检查用户名是否已经存在 967 | # exist_user = await User.objects.get(user_id=old_user_id) 968 | # if exist_user is not None: 969 | # return ConstData.msg_exist 970 | # 971 | # # 没有修改前,可以使用user_id来检查 972 | # current_auth_user = await User.objects.get(id=self.cache_session.user.get_id()) 973 | # """:type:User""" 974 | # # mis_wechat_user = await MisWeChatUser.objects.get(user_id=self.cache_log_session.user_id) 975 | # # """:type:MisWeChatUser""" 976 | # 977 | # # 非初始状态,不允许修改 978 | # if current_auth_user.status != UserStatus.init: 979 | # return ConstData.msg_forbidden 980 | # 981 | # current_auth_user.user_id = old_user_id 982 | # current_auth_user.status = UserStatus.user_id_changed 983 | # current_auth_user = await current_auth_user.save() 984 | # 985 | # # # 修改和微信相关联的信息 986 | # # mis_wechat_user.user_id = new_user_id 987 | # # await mis_wechat_user.save() 988 | # 989 | # # 更新session里面的内容 990 | # old_session = await WebToken.objects.get(id=self.cache_session.get_id()) 991 | # """:type:WebToken""" 992 | # old_session.user_id = old_user_id 993 | # await old_session.save() 994 | # 995 | # res_dict = dict( 996 | # user_id=current_auth_user.user_id, 997 | # nickname=current_auth_user.nickname, 998 | # rc_time=current_auth_user.rc_time 999 | # ) 1000 | # 1001 | # return get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 1002 | # 1003 | # 1004 | # class SetAuthUserId(MyUserBaseHandler): 1005 | # # TODO: change to motor 1006 | # """ 1007 | # 设置当前登录用户的ID,只能修改一次 1008 | # """ 1009 | # 1010 | # @token_required() 1011 | # @my_async_jsonp 1012 | # async def post(self): 1013 | # req_dict = self.get_post_body_dict() 1014 | # new_user_id = req_dict.get('new_user_id', None) 1015 | # 1016 | # if list_have_none_mem(*[new_user_id, ]): 1017 | # return ConstData.msg_args_wrong 1018 | # 1019 | # # 做一些密码复杂性和合法性检查 1020 | # if not user_id_is_legal(new_user_id): 1021 | # return ConstData.msg_args_wrong 1022 | # 1023 | # # 检查用户名是否已经存在 1024 | # exist_user = await User.objects.get(user_id=new_user_id) 1025 | # if exist_user is not None: 1026 | # return ConstData.msg_exist 1027 | # 1028 | # # 没有修改前,可以使用user_id来检查 1029 | # current_auth_user = await User.objects.get(id=self.cache_session.user.get_id()) 1030 | # """:type:User""" 1031 | # # mis_wechat_user = await MisWeChatUser.objects.get(user_id=self.cache_log_session.user_id) 1032 | # # """:type:MisWeChatUser""" 1033 | # 1034 | # # 非初始状态,不允许修改 1035 | # if current_auth_user.status != UserStatus.init.value: 1036 | # return ConstData.msg_forbidden 1037 | # 1038 | # current_auth_user.user_id = new_user_id 1039 | # current_auth_user.status = UserStatus.user_id_changed.value 1040 | # current_auth_user = await current_auth_user.save() 1041 | # 1042 | # # # 修改和微信相关联的信息 1043 | # # mis_wechat_user.user_id = new_user_id 1044 | # # await mis_wechat_user.save() 1045 | # 1046 | # # 更新session里面的内容 1047 | # old_session = await WebToken.objects.get(id=self.cache_session.get_id()) 1048 | # """:type:WebToken""" 1049 | # old_session.user_id = new_user_id 1050 | # await old_session.save() 1051 | # 1052 | # res_dict = dict( 1053 | # user_id=current_auth_user.user_id, 1054 | # nickname=current_auth_user.nickname, 1055 | # rc_time=current_auth_user.rc_time 1056 | # ) 1057 | # 1058 | # return get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 1059 | # 1060 | # 1061 | # class GetMobileToken(MyUserBaseHandler): 1062 | # # TODO: change to motor 1063 | # """ 1064 | # 从PC引导到手机上,然后创建一个refresh_token 1065 | # 1066 | # 1. 如果不存在手机refresh_token,新增加一个,如果已经存在,则更新时间 1067 | # 2. 返回refresh_token给手机端,让手机来获取 access_token 1068 | # 3. 获取了 mobile_token 之后,表示pam_token已经被使用,则删除掉 1069 | # """ 1070 | # 1071 | # @my_async_jsonp 1072 | # async def get(self): 1073 | # qrtoken = self.get_argument('qrtoken', None) 1074 | # if qrtoken is None: 1075 | # return ConstData.msg_args_wrong 1076 | # 1077 | # pam_token = await PcAuthMobileToken.objects.get(token=qrtoken) 1078 | # """:type:PcAuthMobileToken""" 1079 | # if pam_token is None: 1080 | # return ConstData.msg_anonymous 1081 | # 1082 | # user = pam_token.user 1083 | # 1084 | # mobile_token = await self.create_token_session(user, MobileToken, 1085 | # client_type=ClientTypeDict.mobile 1086 | # ) 1087 | # """:type:MobileToken""" 1088 | # 1089 | # if mobile_token is not None: 1090 | # await pam_token.complete_del() 1091 | # 1092 | # return get_std_json_response(data=jsontool.dumps(mobile_token.to_dict())) 1093 | # 1094 | # 1095 | # class MobileAuthPc(MyUserBaseHandler): 1096 | # # TODO: change to motor 1097 | # """ 1098 | # 登录的手机给PC授权 1099 | # """ 1100 | # 1101 | # @token_required() 1102 | # @my_async_jsonp 1103 | # async def get(self): 1104 | # 1105 | # qrtoken = self.get_argument('qrtoken', None) 1106 | # 1107 | # user = self.get_current_session_user() # 当前手机登录的用户 1108 | # 1109 | # if qrtoken is None: 1110 | # return ConstData.msg_args_wrong 1111 | # 1112 | # map_token = await MobileAuthPcToken.objects.get(token=qrtoken) 1113 | # """:type:MobileAuthPcToken""" 1114 | # 1115 | # if map_token is None: 1116 | # return ConstData.msg_none 1117 | # 1118 | # # 授权,用户签名 1119 | # map_token.user = user 1120 | # map_token.user_id = user.user_id 1121 | # map_token.u_name = user.nickname 1122 | # 1123 | # await map_token.save() # 成功签名后保存 1124 | # return ConstData.msg_succeed 1125 | 1126 | 1127 | # class GetMobileAccessToken(MyUserBaseHandler): 1128 | # # TODO: change to motor 1129 | # """ 1130 | # 通过 refresh_token 来获取 access_token 1131 | # 1132 | # - 根据 refresh_token 获取 access_token 1133 | # - 如果不存在,则建立移动端的 token 1134 | # - 如果存在,则更新时间 1135 | # - 不管是否存在,都会返回一个 access_token 1136 | # """ 1137 | # 1138 | # @my_async_jsonp 1139 | # async def get(self): 1140 | # refresh_token = self.get_argument('rtoken', None) 1141 | # if refresh_token is None: 1142 | # return ConstData.msg_args_wrong 1143 | # 1144 | # refresh_session = await MobileToken.objects.get(token=refresh_token) 1145 | # """:type:MobileToken""" 1146 | # if refresh_session is None: 1147 | # return ConstData.msg_anonymous 1148 | # 1149 | # user = refresh_session.user 1150 | # 1151 | # acc_token = await self.create_token_session(user, AccessToken, client_type=ClientTypeDict.mobile) 1152 | # """:type:AccessToken""" 1153 | # 1154 | # return get_std_json_response(data=jsontool.dumps(acc_token.to_dict())) 1155 | # 1156 | # 1157 | # class LanApp(MyUserBaseHandler): 1158 | # # TODO: change to motor 1159 | # """ 1160 | # 重定向到内网app 1161 | # """ 1162 | # 1163 | # @token_required() 1164 | # @my_async_jsonp 1165 | # async def get(self): 1166 | # token = self.get_token() 1167 | # lan_user = await LanAppUser.objects.get(user=self.get_current_session_user()) 1168 | # """:type:LanAppUser""" 1169 | # if lan_user is None: 1170 | # lan_user = LanAppUser() 1171 | # lan_user.is_active = False 1172 | # lan_user.set_user_tag(http_req=self) 1173 | # lan_user.set_default_rc_tag() 1174 | # lan_user = await lan_user.save() 1175 | # 1176 | # if not lan_user.is_active: 1177 | # res_dict = dict( 1178 | # msg='请联系管理员激活内部用户使用权限' 1179 | # ) 1180 | # 1181 | # std_res = get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 1182 | # 1183 | # return std_res 1184 | # 1185 | # lan_url = '%s/dist/index.html?token=%s' % (capt_image_domain, token) 1186 | # 1187 | # res_dict = dict( 1188 | # url=lan_url 1189 | # ) 1190 | # 1191 | # std_res = get_std_json_response(data=jsontool.dumps(res_dict)) 1192 | # return std_res 1193 | # 1194 | # 1195 | # class Feedback(MyUserBaseHandler): 1196 | # # TODO: change to motor 1197 | # """ 1198 | # 用户反馈信息 1199 | # """ 1200 | # 1201 | # @token_required() 1202 | # @my_async_jsonp 1203 | # async def post(self): 1204 | # body_dict = self.get_post_body_dict() 1205 | # msg = body_dict.get('msg', None) 1206 | # 1207 | # if list_have_none_mem(*[msg]): 1208 | # return ConstData.msg_args_wrong 1209 | # 1210 | # if len(msg) == 0: # 如果是空字符串,也不允许 1211 | # return ConstData.msg_args_wrong 1212 | # 1213 | # feedback = FeedBackMsg() 1214 | # feedback.msg = msg 1215 | # feedback.label = 'improve' 1216 | # feedback.set_user_tag(http_req=self) 1217 | # feedback.set_default_rc_tag() 1218 | # feedback.set_template() 1219 | # 1220 | # await feedback.save() 1221 | # return ConstData.msg_succeed 1222 | -------------------------------------------------------------------------------- /apps/account/urls.py: -------------------------------------------------------------------------------- 1 | from apps.account.handlers import * 2 | from dtlib import filetool 3 | 4 | app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 5 | 6 | url = [ 7 | # 用户注册登录 8 | (r"/%s/check-env/" % app_path, CheckEnv), 9 | (r"/%s/account-init/" % app_path, AccountInit), 10 | # (r"/%s/create-new-user/" % app_path, CreateUser), # 注册用户 11 | (r"/%s/user-login/" % app_path, UserLogin), # 登入 12 | (r"/%s/user-logout/" % app_path, UserLogout), # 登出 13 | 14 | # 用户信息操作 15 | (r"/%s/get-auth-user-info/" % app_path, GetAuthUserInfo), # 获取当前登录的用户信息 16 | (r"/%s/set-auth-user-info/" % app_path, SetAuthUserInfo), # 设置和修改当前登录的用户信息,可以反复修改 17 | # (r"/%s/set-auth-user-id/" % app_path, SetAuthUserId), # 修改用户ID,只能修改一次,取代默认生成的杂乱的ID 18 | # (r"/%s/set-auth-user-pwd/" % app_path, SetAuthUserPwd), # 设置用户密码 19 | 20 | (r"/%s/get-auth-user-org/" % app_path, GetAuthUserOrganization), # 获取当前用户的当前视图下的组织信息 21 | # (r"/%s/get-auth-user-all-orgs-info/" % app_path, GetAuthUserAllOrganizations), # 获取当前登录的用户的所有的组织信息 22 | 23 | # 用户详细信息操作 24 | (r"/%s/update-user-detail/" % app_path, UpdateUserDetailInfo), # 新增或者修改用户详细信息 25 | (r"/%s/get-user-detail/" % app_path, GetUserDetailInfo), # 新增或者修改用户详细信息 26 | 27 | 28 | # 组织信息的操作 29 | # todo 后面权限要收回到组织管理员 30 | # (r"/%s/create-organization/" % app_path, CreateOrganization), 31 | # (r"/%s/create-default-organization/" % app_path, CreateDefaultOrganization), 32 | # (r"/%s/read-organization/" % app_path, ReadOrganization), 33 | # (r"/%s/update-organization/" % app_path, UpdateOrganization), # 修改组织群 34 | # (r"/%s/delete-organization/" % app_path, DeleteOrganization), 35 | # (r"/%s/get-org-member/" % app_path, GetOrgMember), # 获取组织成员 36 | # (r"/%s/audit-org-member/" % app_path, AuditOrgMember), # 审核用户加入组织的申请 37 | 38 | 39 | # 组织邀请机制 40 | # 复制邀请码产生的加入 41 | # (r"/%s/get-invite-code/" % app_path, GetInviteCode), # todo 后面要删除掉 获取邀请码,如果没有则创建一个 42 | # (r"/%s/invite-user-to-org/" % app_path, InviteUserToOrg), # todo 后面要删除掉 点击此链接,就可以加入此组织 43 | # # 在PC上点击链接产生的邀请 44 | # (r"/%s/get-org-invite-link/" % app_path, GetOrgInviteLink), # 点击此链接,就可以加入此组织 45 | # (r"/%s/accept-org-invite-by-link/" % app_path, AcceptOrgInviteByLink), # 直接打开链接加入组织 46 | # (r"/%s/exit-current-org/" % app_path, ExitCurrentOrg), # 退出当前组织,返回到自己的默认组织 47 | # todo 通过手机扫描二维码产生的邀请加入 48 | 49 | # 移动端 50 | # (r"/%s/mobile-auth-pc-login/" % app_path, MobileAuthPc), # 由手机扫码,对此token签名授权 51 | # (r"/%s/get-mobile-access-token/" % app_path, GetMobileAccessToken), # 由refresh_token获取access_token 52 | 53 | # 其它接口 54 | # (r"/%s/lan-app/" % app_path, LanApp), # 重定向到内网app 55 | 56 | # (r"/%s/feedback/" % app_path, Feedback), # 登录用户提交反馈信息 57 | 58 | ] 59 | -------------------------------------------------------------------------------- /apps/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/admin/__init__.py -------------------------------------------------------------------------------- /apps/admin/decos.py: -------------------------------------------------------------------------------- 1 | # 和权限相关的装饰器 2 | import functools 3 | from dtlib.web.constcls import ConstData 4 | 5 | 6 | def admin_required(is_async=True): 7 | def _async_token_required(method): 8 | @functools.wraps(method) 9 | async def wrapper(self, *args, **kwargs): 10 | """ 11 | 限制用户必须为超管 12 | :param self: 13 | :param args: 14 | :param kwargs: 15 | :return: 16 | """ 17 | log_session = await self.set_session_by_token(cookie_enable=False) 18 | if log_session is None: 19 | self.write(ConstData.msg_unauthorized) 20 | return 21 | 22 | user = log_session['user'] # 获取当前用户 23 | db = self.get_async_mongo() 24 | user_col = db.g_users 25 | user_res = await user_col.find_one({'_id': user}) 26 | if user_res is None or user_res['user_id'] != 'admin': 27 | self.write(ConstData.msg_forbidden) 28 | return 29 | 30 | if is_async: 31 | await method(self, *args, **kwargs) 32 | else: 33 | method(self, *args, **kwargs) 34 | 35 | return wrapper 36 | 37 | return _async_token_required 38 | -------------------------------------------------------------------------------- /apps/admin/handlers.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from dtlib import jsontool 4 | from dtlib.web.decos import deco_jsonp 5 | from dtlib.aio.decos import my_async_paginator, my_async_jsonp 6 | from dtlib.aio.base_mongo import wrap_default_rc_tag 7 | from dtlib.dtlog import dlog 8 | from dtlib.timetool import get_current_utc_time 9 | # from dtlib.tornado.account_docs import MisWeChatUser, User 10 | from dtlib.tornado.decos import token_required 11 | # from dtlib.tornado.docs import ApiCallCounts, LogHistory 12 | # from dtlib.tornado.ttl_docs import WebToken 13 | from dtlib.utils import list_have_none_mem, hashlibmd5with_salt 14 | from dtlib.web.constcls import ConstData 15 | from dtlib.web.tools import get_std_json_response 16 | from pymongo import DESCENDING 17 | 18 | # from apps.admin.decos import perm_check 19 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 20 | from dtlib.tornado.utils import get_org_data 21 | from dtlib.tornado.utils import user_id_is_legal 22 | # from xt_base.document.acl_docs import UserGrpRel 23 | from dtlib.utils import list_have_none_mem, get_rand_salt, hashlibmd5with_salt 24 | from bson import ObjectId 25 | from apps.admin.decos import admin_required 26 | 27 | 28 | class ListOrganization(MyUserBaseHandler): 29 | """ 30 | 创建Organization 31 | """ 32 | 33 | # todo 34 | 35 | def get(self): 36 | self.write('get') 37 | 38 | def post(self): 39 | self.write('post') 40 | 41 | 42 | class ResetAdminPassword(MyUserBaseHandler): 43 | """ 44 | 重置 admin 用户的密码 45 | """ 46 | 47 | # @deco_jsonp 48 | @my_async_jsonp 49 | async def post(self): 50 | super_token = "3fb13a601c4111e8801f448a5b61a7f0bcb70841" 51 | req_dict = self.get_post_body_dict() 52 | new_user_pwd = req_dict.get('new_pwd', None) 53 | s_token_recv = req_dict.get('s_token', None) 54 | if list_have_none_mem(*[s_token_recv, new_user_pwd]): 55 | return ConstData.msg_args_wrong 56 | if super_token != s_token_recv: 57 | return ConstData.msg_forbidden 58 | # 做一些密码复杂性和合法性检查 59 | if not user_id_is_legal(new_user_pwd): 60 | return ConstData.msg_args_wrong 61 | db = self.get_async_mongo() 62 | user_col = db.g_users 63 | old_session = await user_col.find_one({'user_id': 'admin'}) 64 | await user_col.update({'user_id': 'admin'}, 65 | {'$set': {'passwd': hashlibmd5with_salt(new_user_pwd, old_session['salt'])}}) 66 | return ConstData.msg_succeed 67 | 68 | 69 | class AddUser(MyUserBaseHandler): 70 | """ 71 | 添加用户 72 | """ 73 | 74 | def __init__(self, *args, **kwargs): 75 | super(AddUser, self).__init__(*args, **kwargs) 76 | 77 | @admin_required() 78 | @deco_jsonp() 79 | async def post(self, *args, **kwargs): 80 | post_json = self.get_post_body_dict() 81 | user_id = post_json.get('user_id', None) 82 | password = post_json.get('password', None) 83 | nickname = post_json.get('nickname', "默认昵称") 84 | 85 | if list_have_none_mem(*[user_id, password]) or user_id == 'admin': 86 | return ConstData.msg_args_wrong 87 | mongo_conn = self.get_async_mongo() 88 | mycol = mongo_conn['g_users'] 89 | 90 | res = await mycol.find_one({ 91 | 'user_id': user_id 92 | }) 93 | _id = None 94 | if res: 95 | if res['is_del'] is False: 96 | return ConstData.msg_exist 97 | _id = res['_id'] 98 | 99 | rand_salt = get_rand_salt() 100 | data = dict( 101 | nickname=nickname, 102 | user_id=user_id, 103 | passwd=hashlibmd5with_salt(password, rand_salt), 104 | salt=rand_salt, 105 | ) 106 | 107 | data = wrap_default_rc_tag(data) # 加上默认的标签 108 | if _id: 109 | await mycol.update({'_id': _id}, {'$set': data}, upsert=True) 110 | else: 111 | _id = await mycol.insert(data) 112 | 113 | # todo: select orgnization 114 | user_org_col = mongo_conn['user_org_rel'] 115 | res = await user_org_col.find_one({ 116 | 'user': _id 117 | }) 118 | data = dict( 119 | user_name=nickname, 120 | user=_id, 121 | # organization=org_id, 122 | is_active=True, 123 | # org_name=org_name, 124 | is_default=False, 125 | is_owner=False, 126 | is_current=True 127 | ) 128 | if res is None: 129 | org_col = mongo_conn['organization'] 130 | res = await org_col.find_one({ 131 | 'is_del': False, 132 | }) 133 | org_name = res['name'] 134 | org_id = res['_id'] 135 | else: 136 | org_name = res['org_name'] 137 | org_id = res['organization'] 138 | data['org_name'] = org_name 139 | data['organization'] = org_id 140 | 141 | data = wrap_default_rc_tag(data) # 加上默认的标签 142 | await user_org_col.update({'user': _id}, {'$set': data}, upsert=True) 143 | return ConstData.msg_succeed 144 | 145 | 146 | class DeleteUser(MyUserBaseHandler): 147 | """ 148 | 删除用户, 安全删除 149 | """ 150 | 151 | def __init__(self, *args, **kwargs): 152 | super(DeleteUser, self).__init__(*args, **kwargs) 153 | 154 | @admin_required() 155 | @deco_jsonp() 156 | async def post(self, *args, **kwargs): 157 | # todo: judge is admin 158 | post_json = self.get_post_body_dict() 159 | _id = post_json.get('_id', None) 160 | _id = ObjectId(_id) 161 | 162 | if list_have_none_mem(*[_id]): 163 | return ConstData.msg_args_wrong 164 | mongo_conn = self.get_async_mongo() 165 | mycol = mongo_conn['g_users'] 166 | 167 | res = await mycol.find_one({ 168 | '_id': _id 169 | }) 170 | if res: 171 | if res['is_del'] is True or res['user_id'] == 'admin': 172 | return ConstData.msg_args_wrong 173 | 174 | data = dict( 175 | is_del=True 176 | ) 177 | # todo: set del_time 178 | await mycol.update({'_id': ObjectId(_id)}, {'$set': data}, upsert=False) 179 | 180 | # update user_org_rel 181 | user_org_col = mongo_conn['user_org_rel'] 182 | res = await user_org_col.find_one({ 183 | 'user': _id 184 | }) 185 | if res: 186 | data = dict( 187 | is_del=True 188 | ) 189 | await user_org_col.update({'user': _id}, {'$set': data}, upsert=False) 190 | 191 | return ConstData.msg_succeed 192 | 193 | 194 | class LockUser(MyUserBaseHandler): 195 | """ 196 | 锁定用户, 禁止登录 197 | """ 198 | 199 | def __init__(self, *args, **kwargs): 200 | super(LockUser, self).__init__(*args, **kwargs) 201 | 202 | @admin_required() 203 | @deco_jsonp() 204 | async def post(self, *args, **kwargs): 205 | # todo: judge is admin 206 | post_json = self.get_post_body_dict() 207 | _id = post_json.get('_id', None) 208 | is_lock = post_json.get('is_lock', None) 209 | 210 | if list_have_none_mem(*[_id, is_lock]): 211 | return ConstData.msg_args_wrong 212 | mongo_conn = self.get_async_mongo() 213 | mycol = mongo_conn['g_users'] 214 | 215 | res = await mycol.find_one({ 216 | '_id': _id 217 | }) 218 | if res: 219 | if res['is_del'] is True or res['user_id'] == 'admin': 220 | return ConstData.msg_args_wrong 221 | if not isinstance(is_lock, bool): 222 | return ConstData.msg_args_wrong 223 | data = dict( 224 | is_lock=is_lock 225 | ) 226 | await mycol.update({'_id': ObjectId(_id)}, {'$set': data}, upsert=False) 227 | return ConstData.msg_succeed 228 | 229 | 230 | class GetUserList(MyUserBaseHandler): 231 | """ 232 | 获取用户列表 233 | """ 234 | 235 | def __init__(self, *args, **kwargs): 236 | super(GetUserList, self).__init__(*args, **kwargs) 237 | 238 | @admin_required() 239 | @deco_jsonp() 240 | async def get(self, *args, **kwargs): 241 | """ 242 | admin用户请求该接口可以获取所有用户列表 243 | :param self: 244 | :param args: 245 | :param kwargs:col_name 246 | :return: 247 | """ 248 | # todo: admin 249 | show_field = dict( 250 | _id=1, 251 | user_id=1, 252 | nickname=1, 253 | is_lock=1 254 | ) 255 | mongo_conn = self.get_async_mongo() 256 | mycol = mongo_conn['g_users'] 257 | 258 | user_list = mycol.find({ 259 | "is_del": False 260 | }, show_field) # 升序排列 261 | user_cnt = await user_list.count() 262 | user_list = await user_list.to_list(user_cnt) 263 | return get_std_json_response(data=jsontool.dumps(user_list)) 264 | 265 | # code trash (2018-04-23 yx) 266 | # class ListUserInfo(MyUserBaseHandler): 267 | # """ 268 | # 获取所有的用户的信息 269 | # """ 270 | # 271 | # @token_required() 272 | # @deco_jsonp 273 | # @perm_check 274 | # @my_async_paginator 275 | # async def get(self): 276 | # active = self.get_argument('active', None) 277 | # 278 | # if active is '1': 279 | # return await User.objects.filter(active=True).order_by('rc_time', direction=DESCENDING).find_all() 280 | # elif active is '0': 281 | # return await User.objects.filter(active=False).order_by('rc_time', direction=DESCENDING).find_all() 282 | # elif active is None: 283 | # return await User.objects.order_by('rc_time', direction=DESCENDING).find_all() 284 | # 285 | # 286 | # class DeleteUser(MyUserBaseHandler): 287 | # """ 288 | # 删除用户 289 | # """ 290 | # 291 | # @token_required() 292 | # @deco_jsonp 293 | # @perm_check 294 | # async def get(self): 295 | # user_id = self.get_argument("user_id", None) 296 | # if user_id is None: 297 | # return ConstData.msg_args_wrong 298 | # res = await User.objects.filter(user_id=user_id).delete() 299 | # await WebToken.objects.filter(user_id=user_id).delete() 300 | # await MisWeChatUser.objects.filter(user_id=user_id).delete() 301 | # return ConstData.msg_succeed 302 | # 303 | # 304 | # class OnlineUser(MyUserBaseHandler): 305 | # """ 306 | # 在线用户 307 | # """ 308 | # 309 | # @token_required() 310 | # @deco_jsonp 311 | # @perm_check 312 | # @my_async_paginator 313 | # async def get(self): 314 | # user = await WebToken.objects.find_all() 315 | # user_id = [] 316 | # online_user = [] 317 | # for i in user: 318 | # result = i.to_dict() 319 | # user_id.append(result['user_id']) 320 | # for x in user_id: 321 | # user_detail = await User.objects.get(user_id=x) 322 | # if user_detail is not None: 323 | # online_user.append(user_detail) 324 | # else: 325 | # dlog.exception('login user is accidental deleted ') 326 | # return online_user 327 | # 328 | # 329 | # class ListApiCallCount(MyUserBaseHandler): 330 | # """ 331 | # read api调用次数 332 | # """ 333 | # 334 | # @token_required() 335 | # @deco_jsonp 336 | # @perm_check 337 | # @my_async_paginator 338 | # async def get(self): 339 | # return await ApiCallCounts.objects.order_by('counts', direction=DESCENDING).find_all() 340 | # 341 | # 342 | # class GetLogHistory(MyUserBaseHandler): 343 | # """ 344 | # read api调用次数 345 | # """ 346 | # 347 | # @token_required() 348 | # @deco_jsonp 349 | # @perm_check 350 | # @my_async_paginator 351 | # async def get(self): 352 | # return await LogHistory.objects.order_by('rc_time', direction=DESCENDING).find_all() 353 | # 354 | # 355 | # class GetUserCounts(MyUserBaseHandler): 356 | # @token_required() 357 | # @deco_jsonp 358 | # async def get(self): 359 | # """ 360 | # 获取注册用户统计信息: 361 | # 1. 总体注册用户数 362 | # 2. 今日新增用户数 363 | # :return: 364 | # """ 365 | # col_name = 'g_users' 366 | # mongo_conn = self.get_async_mongo() 367 | # mycol = mongo_conn[col_name] 368 | # 369 | # total_cnts = await mycol.find().count() # 总的用户数 370 | # 371 | # now_time = get_current_utc_time() 372 | # start_time = datetime(now_time.year, now_time.month, now_time.day) # 今日凌晨 373 | # today_cnts = await mycol.find({'rc_time': {'$gte': start_time}}).count() # 今日用户数 374 | # 375 | # res_dict = dict( 376 | # total_cnt=total_cnts, 377 | # today_cnt=today_cnts 378 | # ) 379 | # 380 | # return get_std_json_response(data=jsontool.dumps(res_dict, ensure_ascii=False)) 381 | # 382 | # 383 | # class SetUserActiveStatus(MyUserBaseHandler): 384 | # """ 385 | # 设置用户激活信息 386 | # 1. 赋予默认组织 387 | # 2. 设置状态值 388 | # """ 389 | # 390 | # @token_required() 391 | # @deco_jsonp 392 | # async def get(self): 393 | # user_obj_id = self.get_argument('user_obj_id', None) 394 | # is_set_active = self.get_argument('active', None) 395 | # 396 | # if list_have_none_mem(*[user_obj_id, is_set_active]) is True: 397 | # return ConstData.msg_args_wrong 398 | # 399 | # user_info = await User.objects.get(user_id=user_obj_id) 400 | # if is_set_active == '1': 401 | # is_set_active = True 402 | # elif is_set_active == '0': 403 | # is_set_active = False 404 | # await WebToken.objects.filter(user_id=user_obj_id).delete() 405 | # 406 | # if user_info is None: 407 | # return ConstData.msg_none 408 | # 409 | # user_info.active = is_set_active 410 | # await user_info.save() 411 | # return ConstData.msg_succeed 412 | # 413 | # 414 | # class LoginHistory(MyUserBaseHandler): 415 | # """ 416 | # 人工调用接口的日志 417 | # """ 418 | # 419 | # @token_required() 420 | # @deco_jsonp 421 | # @my_async_paginator 422 | # async def get(self): 423 | # """ 424 | # 查询所有的接口延时测试信息 425 | # :return: 426 | # """ 427 | # return get_org_data(self, cls_name=LogHistory) 428 | -------------------------------------------------------------------------------- /apps/admin/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | 运管和超管后台 3 | 4 | 主要包含的功能: 5 | 6 | 1. 注册用户的查看 7 | 2. 各种活动日志 8 | """ 9 | from apps.admin.handlers import * 10 | from dtlib import filetool 11 | 12 | app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 13 | 14 | url = [ 15 | 16 | # 用户信息 17 | # (r"/%s/get-all-user-info/" % app_path, ListUserInfo), # 获取所有的注册用户的基本信息 18 | # (r"/%s/delete-user/" % app_path, DeleteUser), # 删除用户 19 | # (r"/%s/online-user/" % app_path, OnlineUser), # 在线用户 20 | # (r"/%s/get-user-counts/" % app_path, GetUserCounts), # 获取注册用户的统计信息 21 | 22 | # 组织信息 23 | # (r"/%s/get-all-org-info/" % app_path,), # 获取所有组织信息 24 | # (r"/%s/audit-org/" % app_path,), # 审核组织 25 | # (r"/%s/freeze-org/" % app_path,),#冻结组织 26 | 27 | # (r"/%s/set-user-active-status/" % app_path, SetUserActiveStatus), # 设置用户的激活状态 28 | # 29 | # # 接口调用 30 | # (r"/%s/read-api-call-counts/" % app_path, ListApiCallCount), # api调用次数 31 | # 32 | # # 用户活动日志 33 | # (r"/%s/get-log-history/" % app_path, GetLogHistory), 34 | # 35 | # # 超级管理员全网用于监控的日志 36 | # (r"/%s/all-login-his/" % app_path, LoginHistory), # 用户登录 37 | # (r"/%s/all-api-calls-his/" % app_path, ApiCallsHistory), # 接口调用。非自动化的接口调用 38 | 39 | # 自动化的接口调用是高频的,要用单独的统计系统来部署 40 | (r"/%s/list-organization/" % app_path, ListOrganization), # todo 分页列表,这个是所有的组织,属于管理人员的接口 41 | 42 | # 分三层权限控制 43 | # TODO 管理员用于监控自己企业的日志数据 44 | # todo 用户监控自己的日志数据 45 | 46 | (r"/%s/reset-admin-password/" % app_path, ResetAdminPassword), # 使用 super_token 重置 admin 用户的密码 47 | (r"/%s/get-user-list/" % app_path, GetUserList), # 获取用户列表 48 | (r"/%s/add-user/" % app_path, AddUser), # 添加用户 49 | (r"/%s/delete-user/" % app_path, DeleteUser), # 删除用户 50 | (r"/%s/lock-user/" % app_path, LockUser), # 锁定/解锁用户 51 | ] 52 | -------------------------------------------------------------------------------- /apps/auth2/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/auth2/__init__.py -------------------------------------------------------------------------------- /apps/auth2/decos.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from dtlib.tornado.decos import api_counts 4 | from dtlib.web.constcls import ConstData 5 | from dtlib.web.tools import get_jsonp_res 6 | 7 | # from xt_base.document.auth2_docs import AuthAccessToken 8 | 9 | 10 | def async_auth_access_token_required(method): 11 | """ 12 | - 认证平台的access_token 13 | :param method: 14 | :return: 15 | """ 16 | 17 | @api_counts() 18 | @functools.wraps(method) 19 | async def wrapper(self, *args, **kwargs): 20 | """ 21 | :type self MyUserBaseHandler 22 | """ 23 | callback = self.get_argument('callback', None) 24 | token = self.get_argument('token', None) 25 | 26 | self.set_header('Content-Type', 'application/json') 27 | self.set_header('charset', 'utf-8') 28 | 29 | if token is None: 30 | self.write(get_jsonp_res(callback, ConstData.msg_unauthorized)) 31 | return 32 | 33 | self.set_cookie('token', token) 34 | 35 | log_session = await AuthAccessToken.objects.get(token=token) 36 | """:type:AuthAccessToken""" 37 | 38 | if log_session is None: 39 | self.write(get_jsonp_res(callback, ConstData.msg_unauthorized)) 40 | return 41 | 42 | # 在此处直接赋值存储session,一些临时变量在此处缓存 43 | self.token = token # 本次请求都会用到token,相当于会话标识 44 | self.cache_log_session = log_session 45 | 46 | await method(self, *args, **kwargs) 47 | 48 | return wrapper 49 | -------------------------------------------------------------------------------- /apps/auth2/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | 本模块的所有内容都只针对于认证模块的app, 3 | """ 4 | # from aiomotorengine import DateTimeField 5 | # from aiomotorengine import FloatField 6 | # from aiomotorengine import IntField 7 | # from aiomotorengine import ReferenceField 8 | # from aiomotorengine import StringField 9 | # 10 | # from xt_base.document.auth2_docs import ProductResource 11 | # from xt_base.document.source_docs import OrgUserDataDocument 12 | # from dtlib.aio.base_mongo import HttpDocument, ClientTypeDocument, MyDocument 13 | # from dtlib.tornado.account_docs import User 14 | # from dtlib.utils import list_have_none_mem 15 | 16 | # TODO: 后面有需要重写相关功能 17 | 18 | class AuthUser(User): 19 | """ 20 | 目前先直接继承本用户体系的账号 21 | """ 22 | __collection__ = "g_users" 23 | 24 | # class EnterpriseAccount(User): 25 | # """ 26 | # 企业用户账号。 27 | # 一个企业可以创建多个app应用 28 | # 这些app应用共享用户池 29 | # """ 30 | # __collection__ = 'auth_enterprise' 31 | 32 | 33 | class AuthApp(OrgUserDataDocument, ClientTypeDocument): 34 | """ 35 | 开放应用程序,用于做身份认证的 36 | """ 37 | 38 | __collection__ = "auth_app" 39 | __lazy__ = False 40 | 41 | appid = StringField(max_length=32, unique=True) # 公钥 42 | secret = StringField(max_length=32, unique=True) # 私钥 43 | callback = StringField() # 可能是web回调的url或者是回调的app应用包名称,回调的域,用于安全性检查 44 | 45 | async def set_default_tag(self, **kwargs): 46 | """ 47 | 设置默认的标记 48 | :param kwargs: 49 | :return: 50 | """ 51 | client_type = kwargs.get('client_type', None) 52 | http_req = kwargs.get('http_req', None) 53 | 54 | if list_have_none_mem(*[client_type, http_req]): 55 | return None 56 | 57 | await self.set_org_user_tag(http_req=http_req) 58 | self.set_client_type_tag(client_type=client_type) 59 | 60 | # super(AuthApp, self).set_default_rc_tag() 61 | return True 62 | 63 | 64 | class ResourceGroup(MyDocument): 65 | """ 66 | 资源套餐组,不同的套餐组有不同的限额 67 | """ 68 | __collection__ = 'resource_group' 69 | __lazy__ = False 70 | 71 | name = StringField() # 小组名称 72 | 73 | 74 | class OrgResourceGroupRelation(OrgUserDataDocument): 75 | """ 76 | 组织和资源组的关系,不同的资源组有不同的限额 77 | """ 78 | 79 | __collection__ = 'org_resource_grp_rel' 80 | __lazy__ = False 81 | 82 | r_grp = ReferenceField(reference_document_type=ResourceGroup) 83 | rg_name = StringField() # 冗余 84 | 85 | 86 | class ResourceGroupLimit(OrgUserDataDocument): 87 | """ 88 | 每个账号的相应的资源的限额 89 | """ 90 | 91 | __collection__ = 'resource_group_limit' 92 | __lazy__ = False 93 | 94 | resource = ReferenceField(reference_document_type=ProductResource) 95 | r_name = StringField() # 冗余 ,资源名称 96 | limit = IntField() # 目录创建的数目 97 | 98 | 99 | class OrgResourceCnt(OrgUserDataDocument): 100 | """ 101 | 当前组织使用资源的情况 102 | """ 103 | __collection__ = 'org_resource_cnt' 104 | __lazy__ = False 105 | 106 | resource = ReferenceField(reference_document_type=ProductResource) 107 | r_name = StringField() # 冗余 108 | cnt = IntField() 109 | 110 | 111 | # class UserPlatformApp(AuthApp): 112 | # """ 113 | # 用户开放平台应用,需要提供一些东西进行审核的 114 | # """ 115 | # 116 | # company_name = StringField(max_length=1024) # 企业名称 117 | # website = StringField(max_length=1024) # 企业网址 118 | # phone = StringField(max_length=32) # 联系电话 119 | # status = IntField() # 审核状态:0表示刚注册 120 | 121 | # 122 | # class WebOpenApp(AuthApp): 123 | # """ 124 | # PC端的Web扫码登录App,对应的还有移动端的。 125 | # """ 126 | # __collection__ = "auth_app" 127 | # 128 | # cb_url = StringField(max_length=10240) # 回调的url,类似于webhook一样,只要授权后,就调用的url 129 | 130 | 131 | # class MobileOpenApp(AuthApp): 132 | # """ 133 | # 移动端开放平台app 134 | # """ 135 | # __collection__ = "auth_mobile_app" 136 | # cb_app = StringField(max_length=10240) # 回调的app应用包名称 137 | 138 | 139 | # 正面都是系统数据,当然也有可能是业务数据 140 | 141 | class MobileSafetyData(OrgUserDataDocument, HttpDocument, 142 | # OperationDocument 143 | ): 144 | """ 145 | 手机安全数据 146 | 147 | """ 148 | 149 | __collection__ = 'mobile_safety_data' 150 | 151 | bs = StringField() # charging battery status 152 | br = FloatField() # 电池状态,"0.36",battery rate 153 | 154 | carrier = StringField() # 运营商,"\U4e2d\U56fd\U8054\U901a"; 155 | cellular = StringField() # LTE; 156 | coun = StringField() # CN; 157 | dn = StringField() # "iPhone 6s" 158 | 159 | idf = StringField() # 85BA4C903C16F7631; 160 | imei = StringField() # 000000000000000; 161 | # ip = StringField() 162 | lang = StringField() # "en-CN"; 163 | sc_w = IntField() # screen width 164 | sc_h = IntField() # screen height 165 | # mScreen = StringField() # 1242x2208; 166 | dType = StringField() # iPhone; 167 | mac = StringField() # "02:00:00:00"; 168 | dModel = StringField() # "iPhone8,2"; 169 | osType = StringField() # ios; 170 | # osVerInt =StringField()# "9.3.2"; 171 | osVerRelease = StringField() # "9.3.2"; 172 | route = StringField() # "24:3:b0"; 173 | ssid = StringField() # MyWIFI; 174 | # utc = StringField() # "2016-07-18 06:51:53"; 175 | uuid = StringField() # "513DE79D-F20AB2F685A"; 176 | 177 | c_time = DateTimeField() # 客户端时间,东8时区 178 | 179 | # 下面是作为SDK时需要的字段 180 | # gsdkVerCode = "2.16.1.25.1.x"; 181 | # hAppVerCode = 1; 182 | # hAppVerName = "1.0"; 183 | -------------------------------------------------------------------------------- /apps/auth2/handlers.py: -------------------------------------------------------------------------------- 1 | # 这个要移到第三方 2 | from asyncio import sleep 3 | from dtlib import jsontool 4 | from dtlib.aio.decos import my_async_jsonp 5 | from dtlib.randtool import get_uuid1_key, generate_rand_id 6 | from dtlib.timetool import convert_default_str_to_dt_time 7 | from dtlib.tornado.decos import token_required 8 | from dtlib.tornado.ttl_docs import WebToken 9 | from dtlib.utils import list_have_none_mem 10 | from dtlib.web.constcls import ConstData, QrAuthStatusCode 11 | from dtlib.web.tools import get_std_json_response 12 | from dtlib.web.valuedict import ClientTypeDict 13 | 14 | from apps.auth2.decos import async_auth_access_token_required 15 | from config_api import QR_AUTH_DOMAIN, BASE_DOMAIN, AUTH_CALLBACK_DOMAIN 16 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 17 | 18 | # from xt_base.document.auth2_docs import ThirdAuthToken, MobileAuthPcToken, AuthAccessToken, \ 19 | # MobileSafetyData, AuthApp 20 | # from xt_base.document.source_docs import AuthCallbackPage 21 | 22 | try: 23 | from xt_wechat.config import my_auth_app 24 | except: 25 | print("Wechat module didn't installed!") 26 | 27 | 28 | # TODO: 后面有需要重写相关功能 29 | 30 | 31 | class GetMapQrCodeToken(MyUserBaseHandler): 32 | """ 33 | PC端将文本内容返回给客户端,由客户端生成二维码 34 | 35 | 客户端定时请求,每次请求都会导致code发生变化 36 | """ 37 | 38 | @my_async_jsonp 39 | async def get(self): 40 | appid = self.get_argument('appid', None) 41 | 42 | if list_have_none_mem(*[appid]): 43 | return ConstData.msg_args_wrong 44 | 45 | # todo 后续要加一次数据库IO来查询是否存在此qrcode 46 | unauth_token = await self.create_anonymous_token( 47 | MobileAuthPcToken, 48 | appid=appid, 49 | client_type=ClientTypeDict.browser 50 | ) 51 | """:type:MobileAuthPcToken""" 52 | 53 | # 这个链接是给手机打开的,用来给PC签名授权 54 | url = '%s/auth2/app-confirm/' % QR_AUTH_DOMAIN 55 | 56 | res_dict = dict( 57 | appid=appid, 58 | url=url, 59 | qrtoken=unauth_token.token 60 | ) 61 | res_data = get_std_json_response(data=jsontool.dumps(res_dict)) 62 | return res_data 63 | 64 | 65 | class AppConfirm(MyUserBaseHandler): 66 | """ 67 | 手机端扫码后相应的状态提醒。 68 | 如果扫码了,则调出确认的页面,扫码时也改变状态值为410: 69 | 1. 如果确认或者取消后,则使用access_token和uuid去修改状态(注意检查资源从属问题) 70 | #. 如果确认登录后,就进行签名,而且修改状态值为200 71 | """ 72 | 73 | @token_required() 74 | @my_async_jsonp 75 | async def get(self): 76 | uuid = self.get_argument('uuid', None) 77 | confirm = self.get_argument('confirm', None) 78 | 79 | if list_have_none_mem(*[uuid, ]): 80 | return ConstData.msg_args_wrong 81 | 82 | current_auth_token = await MobileAuthPcToken.objects.get(token=uuid) 83 | """:type:MobileAuthPcToken""" 84 | 85 | if current_auth_token is None: 86 | return ConstData.msg_forbidden 87 | 88 | user = self.get_current_session_user() # 当前手机登录的用户 89 | 90 | if confirm is None: 91 | current_auth_token.status = QrAuthStatusCode.wait 92 | elif confirm == '1': 93 | # 将状态修改为 确认授权态,授权,用户签名 94 | current_auth_token.user = user 95 | current_auth_token.user_id = user.user_id 96 | current_auth_token.u_name = user.nickname 97 | current_auth_token.status = QrAuthStatusCode.confirm 98 | 99 | # 然后创建auth_token,这个会告诉PC端此用户已经登录,然后把此session传给PC端 100 | # 直接创建webtoken,暂先不客auth_token 101 | await self.create_token_session(user, 102 | WebToken, 103 | client_type=ClientTypeDict.browser 104 | ) # 创建一个PC端的access_token 105 | else: 106 | # 将状态修改为 取消授权态 107 | current_auth_token.status = QrAuthStatusCode.cancel 108 | 109 | current_auth_token = await current_auth_token.save() 110 | 111 | return get_std_json_response(data=jsontool.dumps(current_auth_token.to_dict())) 112 | 113 | 114 | # region 以下就是PC端后续的调用接口 115 | 116 | class GetAuthAccessToken(MyUserBaseHandler): 117 | """ 118 | auth2.0里面的获取认证信息,因为二维码信息容易被获取,所以中间再加一个auth_token 119 | :需要私钥(影子)。此时是可信的服务器作为通讯双方 120 | """ 121 | 122 | @my_async_jsonp 123 | async def get(self): 124 | appid = self.get_argument('appid', None) # 应用公钥 125 | shadow_secret = self.get_argument('shadow_secret', None) # 影子私钥 126 | auth_token = self.get_argument('auth_token', None) # 用户授权token 127 | 128 | if list_have_none_mem(*[appid, shadow_secret, auth_token]): 129 | return ConstData.msg_args_wrong 130 | 131 | auth_token = await ThirdAuthToken.objects.get(token=auth_token) 132 | """:type:ThirdAuthToken""" 133 | if auth_token is None: 134 | return ConstData.msg_none 135 | 136 | access_token = await AuthAccessToken.objects.get(user=auth_token.user) 137 | """:type:AuthAccessToken""" 138 | 139 | res_data = get_std_json_response(data=jsontool.dumps(access_token.to_dict())) 140 | return res_data 141 | 142 | 143 | class GetAuthUserInfo(MyUserBaseHandler): 144 | """ 145 | 另外的一套独立的token体系,后面会独立出来 146 | """ 147 | 148 | @async_auth_access_token_required 149 | async def get(self): 150 | """ 151 | 返回信息里面有用户针对此应用的unionid信息,方便第三方应用系统A做好账号映射工作 152 | :return: 153 | """ 154 | await sleep() 155 | 156 | 157 | class UploadMobileSafetyData(MyUserBaseHandler): 158 | """ 159 | 获取手机风控相关数据 160 | """ 161 | 162 | @token_required() 163 | @my_async_jsonp 164 | async def post(self): 165 | mobile_safety_data = self.get_post_body_dict() 166 | 167 | if mobile_safety_data is None: 168 | return ConstData.msg_args_wrong 169 | 170 | print(jsontool.dumps(mobile_safety_data)) 171 | 172 | # operation_key = mobile_safety_data.get('op', None) # operation 桩,整数:0,1,2,3 173 | mobile_info = mobile_safety_data.get('mi', None) # 收集的手机静态信息,mobile info 174 | apps_info = mobile_safety_data.get('ai', None) # 安装的应用信息, apps info 175 | client_time_str = mobile_safety_data.get('tm', None) # time 176 | # todo 后面存储应用的数据信息 177 | 178 | # operation_dict = OperationDict.get_value_by_attrib_name(operation_key) 179 | 180 | if list_have_none_mem(*[mobile_info, apps_info, client_time_str, 181 | # operation_dict 182 | ]): 183 | return ConstData.msg_args_wrong 184 | 185 | ms_data = MobileSafetyData() 186 | ms_data.bs = mobile_info.get('bs', None) 187 | 188 | # 电池电量 189 | battery_rate = mobile_info.get('br', None) 190 | if battery_rate is not None: 191 | ms_data.br = float(battery_rate) 192 | 193 | ms_data.carrier = mobile_info.get('carrier', None) 194 | ms_data.cellular = mobile_info.get('cellular', None) 195 | ms_data.coun = mobile_info.get('coun', None) 196 | ms_data.dn = mobile_info.get('dn', None) 197 | ms_data.idf = mobile_info.get('idf', None) 198 | ms_data.imei = mobile_info.get('imei', None) 199 | ms_data.lang = mobile_info.get('lang', None) 200 | 201 | # 屏幕分辨率 202 | screen_width = mobile_info.get('dw', None) 203 | screen_height = mobile_info.get('dh', None) 204 | if not list_have_none_mem(*[screen_height, screen_width]): 205 | ms_data.sc_h = int(screen_height) 206 | ms_data.sc_w = int(screen_width) 207 | 208 | ms_data.dType = mobile_info.get('dType', None) 209 | ms_data.mac = mobile_info.get('mac', None) 210 | ms_data.dModel = mobile_info.get('dModel', None) 211 | ms_data.osType = mobile_info.get('osType', None) 212 | ms_data.osVerRelease = mobile_info.get('osVerRelease', None) 213 | ms_data.route = mobile_info.get('route', None) 214 | ms_data.ssid = mobile_info.get('ssid', None) 215 | ms_data.uuid = mobile_info.get('uuid', None) 216 | 217 | client_time = convert_default_str_to_dt_time(client_time_str) 218 | ms_data.c_time = client_time 219 | 220 | # 设置组织和人员归属关系 221 | await ms_data.set_org_user_tag(http_req=self) 222 | ms_data.set_http_tag(http_req=self) 223 | # 设置操作类型 224 | # ms_data.set_operation_tag(operation_dict) 225 | 226 | await ms_data.save() 227 | 228 | return ConstData.msg_succeed 229 | 230 | 231 | class CreateAuthApp(MyUserBaseHandler): 232 | """ 233 | 建议auth应用 234 | """ 235 | 236 | @token_required() 237 | @my_async_jsonp 238 | async def get(self): 239 | client_name = self.get_argument('client', None) 240 | if client_name is None: 241 | return ConstData.msg_args_wrong 242 | 243 | client_type = ClientTypeDict.get_value_by_attrib_name(client_name) 244 | if client_type is None: 245 | return ConstData.msg_args_wrong 246 | 247 | auth_app = AuthApp() 248 | 249 | uuid_key = get_uuid1_key() 250 | 251 | auth_app.appid = uuid_key 252 | auth_app.secret = generate_rand_id(sstr=uuid_key) 253 | auth_app.callback = BASE_DOMAIN 254 | 255 | await auth_app.set_default_tag(client_type=client_type, http_req=self) 256 | await auth_app.save() 257 | 258 | return ConstData.msg_succeed 259 | 260 | 261 | # endregion 262 | 263 | 264 | class CreateAuthCallbackPage(MyUserBaseHandler): 265 | """ 266 | 创建page 267 | """ 268 | 269 | @token_required() 270 | @my_async_jsonp 271 | async def get(self): 272 | auth_cb_page = AuthCallbackPage() 273 | auth_cb_page.url = AUTH_CALLBACK_DOMAIN 274 | await auth_cb_page.set_org_user_tag(http_req=self) 275 | auth_cb_page.set_template() 276 | auth_cb_page.set_default_rc_tag() 277 | await auth_cb_page.save() 278 | return ConstData.msg_succeed 279 | -------------------------------------------------------------------------------- /apps/auth2/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这里面有类似于微信客户端扫码登录的全套解决方案。但是也有区别: 3 | 4 | 1. 本方法不是使用重定向,而是使用回调函数。所以不定型是登录,还可以用作为签名等场合。 5 | 6 | """ 7 | # from apps.auth2.handlers import * 8 | # from dtlib import filetool 9 | # 10 | # app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 11 | # 12 | # url = [ 13 | # 14 | # # PC端的授权页面 15 | # (r"/%s/get-map-qrcode-token/" % app_path, GetMapQrCodeToken), # PC上生成串,然后由客户端js生成qrcode,客户端生成二维码js只有10K 16 | # 17 | # # 根据二维码授权结果,以字符形式的auth_token再换取第三方的access_token, 18 | # (r"/%s/get-auth-access-token/" % app_path, GetAuthAccessToken), 19 | # 20 | # # 移动授权app客户端,手机端在扫码后,打开二维码所在页面 21 | # (r"/%s/app-confirm/" % app_path, AppConfirm), # 这是二维码中的具体文字内容,手机端的确认 22 | # 23 | # # 网站主服务器 24 | # (r"/%s/get-auth-user-info" % app_path, GetAuthUserInfo), 25 | # 26 | # # todo 手机端注册 27 | # # . 输入邮箱,再轮询查看认证状态。 28 | # # . 输入手机号码,再发送验证码 29 | # # . 优先:直接绑定微信 30 | # 31 | # # 创建开发平台auth应用 32 | # (r"/%s/create-auth-app/" % app_path, CreateAuthApp), 33 | # 34 | # # 移动端数据安全 35 | # (r"/%s/up-ms-data/" % app_path, UploadMobileSafetyData), # 获取手机风控数据,在登录的时候和手机放一起 36 | # 37 | # # pc上面生成二维码,给手机扫描,由已经认证了的PC给手机进行授权--(这里面的功能给废弃掉) 38 | # # (r"/%s/get-pam-token-qrcode/" % app_path, GetPamTokenQrCode), # PC上生成的二维码图片(1分钟刷新一次),让手机扫描 39 | # # (r"/%s/get-mobile-token/" % app_path, GetMobileToken), # 由手机发起的请求,获取和刷新access_token 40 | # 41 | # # 由未授权PC上生成二维码,然后授权之后的手机扫码 42 | # # (r"/%s/get-map-token-qrcode/" % app_path, GetMapTokenQrCode), # PC上生成的二维码图片(1分钟刷新一次),让手机扫描(此种在服务端生成二维码的方案废弃掉) 43 | # # (r"/%s/pc-get-auth-result/" % app_path, PcGetAuthResult), # PC端轮询获取授权的结果 44 | # 45 | # # todo auth callback page的 CRUD 46 | # (r"/%s/create-auth-cb-page/" % app_path, CreateAuthCallbackPage), 47 | # # (r"/%s/get-auth-cb-page/" % app_path, GetAuthCallbackPage), 48 | # ] 49 | 50 | url = [] 51 | # 暂时注释掉, 似乎没用到 (2018-04-23) 52 | -------------------------------------------------------------------------------- /apps/base/__init__.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | the common base handlers 5 | """ 6 | __author__ = 'zheng' 7 | -------------------------------------------------------------------------------- /apps/base/handlers.py: -------------------------------------------------------------------------------- 1 | import dtlib 2 | from dtlib import jsontool 3 | from dtlib import timetool 4 | from dtlib.mongo.utils import get_mongodb_version, get_pip_list, get_python_version 5 | from dtlib.tornado.decos import test_token_required, api_counts 6 | from dtlib.web.decos import deco_jsonp 7 | from dtlib.web.tools import get_std_json_response 8 | from tornado.web import RequestHandler 9 | 10 | from config import app_version, test_token 11 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 12 | 13 | 14 | class MainHandler(RequestHandler): 15 | """ 16 | 本接口的首页信息 17 | """ 18 | 19 | def get(self): 20 | self.write('This the Home Page') 21 | 22 | 23 | class AppInfo(MyUserBaseHandler): 24 | """ 25 | 获取本web应用的基本信息 26 | """ 27 | 28 | @api_counts() 29 | @deco_jsonp(is_async=False) 30 | def get(self): 31 | print('run app-info:', app_version) 32 | res = dict( 33 | server='tornado', # web服务器 34 | app_version=app_version, # 应用版本 35 | dtlib_version=dtlib.VERSION, # 第三方库dt-lib的版本 36 | req_time=timetool.get_current_time_string(), # 当前查询时间点 37 | timezone=timetool.get_time_zone(), # 当前服务器的时区 38 | ) 39 | return get_std_json_response(data=jsontool.dumps(res, ensure_ascii=False)) 40 | 41 | 42 | class PrivateAppInfo(MyUserBaseHandler): 43 | """ 44 | 获取本web应用的基本信息,这些需要有管理员的test_key才能访问 45 | """ 46 | 47 | @test_token_required(test_token=test_token) 48 | @deco_jsonp(is_async=False) 49 | def get(self): 50 | print('run app-info:', app_version) 51 | 52 | mongo_version = get_mongodb_version() 53 | pip_list = get_pip_list() 54 | python = get_python_version() 55 | 56 | res = dict( 57 | server='tornado', # web服务器 58 | app_version=app_version, # 应用版本 59 | dtlib_version=dtlib.VERSION, # 第三方库dt-lib的版本 60 | req_time=timetool.get_current_time_string(), # 当前查询时间点 61 | timezone=timetool.get_time_zone(), # 当前服务器的时区 62 | 63 | mongo_server=mongo_version, # mongo的服务器版本号 64 | python=python, # python版本号 65 | pip_list=pip_list, # 安装的pip_list 66 | ) 67 | return get_std_json_response(data=jsontool.dumps(res, ensure_ascii=False)) 68 | 69 | 70 | class GetClientInfo(MyUserBaseHandler): 71 | @deco_jsonp(is_async=False) 72 | def get(self): 73 | """ 74 | 返回客户端的公网IP地址及端口 75 | :return: 76 | """ 77 | 78 | client_req = self.request 79 | 80 | res_dict = dict( 81 | remote_ip=client_req.remote_ip, 82 | method=client_req.method, 83 | path=client_req.path, 84 | headers=client_req.headers._dict 85 | 86 | ) 87 | 88 | return get_std_json_response(data=jsontool.dumps(res_dict)) 89 | 90 | 91 | class CreateServerAsset(MyUserBaseHandler): 92 | """ 93 | 创建服务器资产 94 | """ 95 | 96 | async def post(self): 97 | return 'hello' 98 | -------------------------------------------------------------------------------- /apps/base/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | 任何一个Web系统里面都需要的一些提供系统信息的工具集 3 | """ 4 | from apps.base.handlers import * 5 | from dtlib import filetool 6 | 7 | app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 8 | 9 | url = [ 10 | # 公用 11 | (r"/", MainHandler), 12 | (r"/app-info/", AppInfo), 13 | (r"/private-app-info/", PrivateAppInfo), # 一些隐私一些的应用信息 14 | 15 | (r"/get-client-info/", GetClientInfo), # 获取客户端请求信息,包括IP地址 16 | (r"/create-server-asset/", CreateServerAsset), # 创建信息服务器资产 17 | ] 18 | -------------------------------------------------------------------------------- /apps/dashboard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/dashboard/__init__.py -------------------------------------------------------------------------------- /apps/dashboard/handlers.py: -------------------------------------------------------------------------------- 1 | # from dtlib.web.tools import get_std_json_response 2 | # from dtlib import jsontool 3 | # from bson import ObjectId 4 | # from pymongo import DESCENDING 5 | # from functools import reduce 6 | import time 7 | from pymongo import DESCENDING 8 | from bson import ObjectId 9 | from dtlib import jsontool 10 | from dtlib.web.decos import deco_jsonp 11 | from dtlib.web.constcls import ConstData, ResCode 12 | from dtlib.web.tools import get_std_json_response 13 | from dtlib.utils import list_have_none_mem 14 | from dtlib.aio.base_mongo import wrap_default_rc_tag 15 | from dtlib.tornado.decos import token_required 16 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 17 | 18 | import json 19 | 20 | 21 | class UpdateContent(MyUserBaseHandler): 22 | """ 23 | 更新文本内容 24 | """ 25 | 26 | def __init__(self, *args, **kwargs): 27 | super(UpdateContent, self).__init__(*args, **kwargs) 28 | 29 | @token_required() 30 | @deco_jsonp() 31 | async def post(self, *args, **kwargs): 32 | post_json = self.get_post_body_dict() 33 | content_id = post_json.get('content_id', None) 34 | pro_id = post_json.get('pro_id', "5a7fb0cd47de9d5cf3d13a44") 35 | group = post_json.get('group', "test") 36 | time_stamp = post_json.get('date_time', None) 37 | content = post_json.get('content', None) 38 | 39 | if list_have_none_mem(*[pro_id, group, time_stamp, content]): 40 | return ConstData.msg_args_wrong 41 | mongo_conn = self.get_async_mongo() 42 | mycol = mongo_conn['dashboard_content'] 43 | timeArray = time.localtime(int(time_stamp) / 1000.0) 44 | date_time = time.strftime("%Y-%m-%d %H:%M:%S", timeArray) 45 | data = dict( 46 | pro_id=ObjectId(pro_id), 47 | group=group, 48 | date_time=date_time, 49 | content=content, 50 | ) 51 | data = wrap_default_rc_tag(data) # 加上默认的标签 52 | if content_id: 53 | await mycol.update({'_id': ObjectId(content_id)}, {'$set': data}, upsert=True) 54 | return ConstData.msg_succeed 55 | else: 56 | _id = await mycol.insert(data) 57 | msg_succeed = '{"code":%s,"msg":"%s","data":{"_id": "%s"}}' % (ResCode.ok, "success", _id) 58 | return msg_succeed 59 | 60 | 61 | class DeleteContent(MyUserBaseHandler): 62 | """ 63 | 删除文本内容 64 | """ 65 | 66 | def __init__(self, *args, **kwargs): 67 | super(DeleteContent, self).__init__(*args, **kwargs) 68 | 69 | @token_required() 70 | @deco_jsonp() 71 | async def post(self, *args, **kwargs): 72 | post_json = self.get_post_body_dict() 73 | content_id = post_json.get('content_id', None) 74 | # pro_id = self.get_argument('pro_id', "5a7fb0cd47de9d5cf3d13a44") 75 | if list_have_none_mem(*[content_id]): 76 | return ConstData.msg_args_wrong 77 | mongo_conn = self.get_async_mongo() 78 | mycol = mongo_conn['dashboard_content'] 79 | mycol.update({'_id': ObjectId(content_id)}, {'$set': {'is_del': True}}, upsert=False) 80 | return ConstData.msg_succeed 81 | 82 | 83 | class GetContent(MyUserBaseHandler): 84 | def __init__(self, *args, **kwargs): 85 | super(GetContent, self).__init__(*args, **kwargs) 86 | 87 | @token_required() 88 | @deco_jsonp() 89 | async def get(self, *args, **kwargs): 90 | hide_fields = { 91 | 'rc_time': 0, 92 | 'del_time': 0, 93 | 'is_del': 0, 94 | 'pro_id': 0 95 | } 96 | return await self.get_content_list(col_name='dashboard_content', hide_fields=hide_fields) 97 | 98 | async def get_content_list(self, *args, **kwargs): 99 | """ 100 | 直接传入项目名称和表的名称,返回分页的信息 101 | :param self: 102 | :param args: 103 | :param kwargs:col_name 104 | :return: 105 | """ 106 | page_cap = self.get_argument('page_cap', '10') # 一页的记录容量-var 107 | pro_id = self.get_argument('pro_id', None) 108 | 109 | col_name = kwargs.get('col_name', None) 110 | hide_fields = kwargs.get('hide_fields', None) # 需要隐藏的字段,一个dict 111 | """:type:dict""" 112 | 113 | if list_have_none_mem(*[col_name, pro_id, page_cap]): 114 | return ConstData.msg_args_wrong 115 | page_cap = int(page_cap) 116 | mongo_conn = self.get_async_mongo() 117 | mycol = mongo_conn[col_name] 118 | 119 | if pro_id is None: 120 | msg_details = mycol.find({ 121 | "is_del": False 122 | }, hide_fields).sort([('date_time', DESCENDING)]) # 升序排列 123 | else: 124 | msg_details = mycol.find({ 125 | 'pro_id': ObjectId(pro_id), 126 | "is_del": False 127 | }, hide_fields).sort([('date_time', DESCENDING)]) # 升序排列 128 | 129 | msg_details_cnt = await msg_details.count() 130 | page_size = page_cap if page_cap < msg_details_cnt else msg_details_cnt 131 | msg_content_list = await msg_details.to_list(page_size) 132 | return get_std_json_response(data=jsontool.dumps(msg_content_list)) 133 | 134 | 135 | class UpdateProjectShow(MyUserBaseHandler): 136 | """ 137 | 更新项目是否展示的状态 138 | """ 139 | 140 | def __init__(self, *args, **kwargs): 141 | super(UpdateProjectShow, self).__init__(*args, **kwargs) 142 | 143 | @deco_jsonp() 144 | async def post(self, *args, **kwargs): 145 | post_json = self.get_post_body_dict() 146 | tv_tags = post_json.get('tv_tags', None) 147 | pro_id = post_json.get('pro_id', None) 148 | 149 | if list_have_none_mem(*[tv_tags, pro_id]): 150 | return ConstData.msg_args_wrong 151 | if not isinstance(tv_tags, list): 152 | return ConstData.msg_args_wrong 153 | mongo_conn = self.get_async_mongo() 154 | mycol = mongo_conn['test_project'] 155 | project = await mycol.find_one({'_id': ObjectId(pro_id)}, {'tags': 1}) 156 | if 'tags' in project: 157 | tags = project['tags'] 158 | if set(tv_tags).issubset(set(tags)) is False: 159 | return ConstData.msg_args_wrong 160 | await mycol.update({'_id': ObjectId(pro_id)}, {'$set': {'tv_tags': tv_tags}}, upsert=False) 161 | return ConstData.msg_succeed 162 | -------------------------------------------------------------------------------- /apps/dashboard/urls.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | """ 3 | websocket相关接口 4 | """ 5 | 6 | from apps.dashboard.handlers import UpdateContent, GetContent, DeleteContent, UpdateProjectShow 7 | from dtlib import filetool 8 | 9 | app_path = filetool.get_parent_folder_name(__file__) 10 | 11 | url = [ 12 | (r"/%s/update_content/" % app_path, UpdateContent), # 更新项目信息 13 | (r"/%s/delete_content/" % app_path, DeleteContent), # 删除项目信息 14 | (r"/%s/get_content/" % app_path, GetContent), # 获取项目信息 15 | (r"/%s/update_project_show/" % app_path, UpdateProjectShow), # 更新展示状态 16 | ] 17 | -------------------------------------------------------------------------------- /apps/project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/project/__init__.py -------------------------------------------------------------------------------- /apps/project/handlers.py: -------------------------------------------------------------------------------- 1 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 2 | from bson import ObjectId 3 | from dtlib import jsontool 4 | from dtlib.aio.decos import my_async_jsonp, my_async_paginator 5 | from dtlib.tornado.decos import token_required 6 | from dtlib.utils import list_have_none_mem 7 | from dtlib.web.constcls import ConstData, ResCode 8 | from dtlib.web.tools import get_std_json_response 9 | 10 | from dtlib.tornado.utils import get_org_data 11 | from dtlib.tornado.utils import get_org_data_paginator 12 | 13 | from dtlib.web.decos import deco_jsonp 14 | from dtlib.tornado.utils import set_default_rc_tag 15 | from apps.admin.decos import admin_required 16 | 17 | 18 | class ListProjectsNote(MyUserBaseHandler): 19 | """ 20 | 根据当前组织得到appid,key 21 | """ 22 | 23 | @token_required() 24 | @my_async_jsonp 25 | async def get(self): 26 | project_id = self.get_argument('project_id', None) 27 | if project_id is None: 28 | return ConstData.msg_args_wrong 29 | db = self.get_async_mongo() 30 | proj_col = db.test_project 31 | project_obj = await proj_col.find_one({'_id': ObjectId(str(project_id))}) 32 | organization = project_obj['organization'] 33 | app_col = db.test_data_app 34 | test_data_app = await app_col.find_one({'organization': organization}) 35 | data = dict( 36 | project_name=project_obj['project_name'], 37 | project_id=project_obj['_id'], 38 | app_id=test_data_app['app_id'], 39 | app_key=test_data_app['app_key'] 40 | ) 41 | res_str = get_std_json_response(code=200, data=jsontool.dumps(data)) 42 | 43 | return res_str 44 | 45 | 46 | class CreateTestProject(MyUserBaseHandler): 47 | """ 48 | 项目名称 49 | """ 50 | 51 | @token_required() 52 | @my_async_jsonp 53 | async def post(self): 54 | post_json = self.get_post_body_dict() 55 | 56 | mark = post_json.get('mark', None) 57 | project_name = post_json.get('name', None) 58 | 59 | organization = await self.get_organization() 60 | user = self.get_current_session_user() 61 | 62 | db = self.get_async_mongo() 63 | proj_col = db.test_project 64 | org_col = db.organization 65 | org_res = await org_col.find_one({'_id': ObjectId(organization)}) 66 | org_name = org_res['name'] 67 | user_col = db.g_users 68 | user_res = await user_col.find_one({'_id': ObjectId(user)}) 69 | user_nickname = user_res['nickname'] 70 | new_data = dict( 71 | project_name=project_name, 72 | mark=mark, 73 | organization=organization, 74 | org_name=org_name, 75 | owner=user, 76 | owner_name=user_nickname, 77 | ) 78 | new_data = set_default_rc_tag(new_data) 79 | 80 | result = await proj_col.insert(new_data) 81 | 82 | res_str = get_std_json_response(code=200, data=jsontool.dumps(result)) 83 | 84 | return res_str 85 | 86 | 87 | class UpdateTestProject(MyUserBaseHandler): 88 | """ 89 | 更新project的name 90 | """ 91 | 92 | @admin_required() 93 | @my_async_jsonp 94 | async def post(self): 95 | post_json = self.get_post_body_dict() 96 | 97 | id = post_json.get('id', None) 98 | project_name = post_json.get('name', None) 99 | mark = post_json.get('mark', None) 100 | 101 | if list_have_none_mem(*[id, project_name, mark]): 102 | return ConstData.msg_args_wrong 103 | 104 | # todo 做资源归属和权限的判断 105 | db = self.get_async_mongo() 106 | proj_col = db.test_project 107 | project_obj = await proj_col.find_one({'_id': ObjectId(str(id))}) 108 | user_org = await self.get_organization() 109 | 110 | organization = project_obj['organization'] 111 | if (project_obj is None) or (organization is None): 112 | return ConstData.msg_forbidden 113 | 114 | pro_org_id = organization 115 | if pro_org_id != user_org: 116 | return ConstData.msg_forbidden 117 | 118 | data = dict( 119 | project_name=project_name, 120 | mark=mark 121 | ) 122 | result = await proj_col.update({'_id': ObjectId(str(id))}, {'$set': data}, upsert=False) 123 | res_str = get_std_json_response(code=200, data=jsontool.dumps(result)) 124 | 125 | return res_str 126 | 127 | 128 | class DeleteTestProject(MyUserBaseHandler): 129 | """ 130 | 删除project(实际是将is_del设置为true) 131 | """ 132 | 133 | @admin_required() 134 | @my_async_jsonp 135 | async def get(self): 136 | id = self.get_argument('id', None) 137 | if id is None: 138 | return ConstData.msg_args_wrong 139 | 140 | # todo 做资源归属和权限的判断 141 | db = self.get_async_mongo() 142 | proj_col = db.test_project 143 | test_data_col = db.unit_test_data 144 | project_obj = await proj_col.find_one({'_id': ObjectId(str(id))}) 145 | user_org = await self.get_organization() 146 | 147 | if project_obj is None: 148 | return ConstData.msg_forbidden 149 | 150 | pro_org_id = project_obj['organization'] 151 | if pro_org_id != user_org: 152 | return ConstData.msg_forbidden 153 | 154 | await proj_col.update({'_id': ObjectId(str(id))}, {'$set': {'is_del': True}}, upsert=False) 155 | # 删除附属于项目的测试结果 156 | await test_data_col.update({'pro_id': ObjectId(str(id))}, {'$set': {'is_del': True}}, multi=True) 157 | return ConstData.msg_succeed 158 | 159 | 160 | class ReadProjectsRecord(MyUserBaseHandler): 161 | """ 162 | 根据project—id查找30次的构建情况 163 | """ 164 | 165 | @token_required() 166 | @my_async_jsonp 167 | async def get(self): 168 | id = self.get_argument('id', None) 169 | tag = self.get_argument('tag') 170 | if id is None: 171 | return ConstData.msg_args_wrong 172 | 173 | # todo 做资源归属和权限的判断 174 | db = self.get_async_mongo() 175 | proj_col = db.test_project 176 | project_obj = await proj_col.find_one({'_id': ObjectId(id)}) 177 | user_org = await self.get_organization() 178 | 179 | if project_obj is None: 180 | return ConstData.msg_forbidden 181 | 182 | pro_org_id = project_obj['organization'] 183 | if pro_org_id != user_org: 184 | return ConstData.msg_forbidden 185 | 186 | return await get_org_data_paginator(self, col_name='unit_test_data', pro_id=id, tag=[tag], 187 | hide_fields={'details': 0}) 188 | 189 | 190 | class ListProjects(MyUserBaseHandler): 191 | """ 192 | 本组织的所有的项目列出来 193 | """ 194 | 195 | @token_required() 196 | @my_async_jsonp 197 | @my_async_paginator 198 | async def get(self): 199 | return await get_org_data(self, collection='test_project') 200 | 201 | 202 | class GetProjectTags(MyUserBaseHandler): 203 | """ 204 | 获取某个项目的所有tag 205 | """ 206 | 207 | # @my_async_jsonp 208 | @my_async_jsonp 209 | async def get(self): 210 | pro_id = self.get_argument('pro_id', None) 211 | if pro_id is None: 212 | return ConstData.msg_args_wrong 213 | db = self.get_async_mongo() 214 | pro_col = db.test_project 215 | tags = await pro_col.find_one({'_id': ObjectId(pro_id)}, {'tags': 1}) 216 | # test_data_col = db.unit_test_data 217 | # tags = await test_data_col.distinct("tag", 218 | # {"pro_id": ObjectId(pro_id), 'is_del': False, 'tag': {'$exists': True}}) 219 | tag_list = list() 220 | if 'tags' in tags.keys(): 221 | tag_list = tags['tags'] 222 | msg_succeed = '{"code":%s,"msg":"success","data":%s}' % (ResCode.ok, jsontool.dumps(tag_list)) # 操作成功 223 | return msg_succeed 224 | -------------------------------------------------------------------------------- /apps/project/urls.py: -------------------------------------------------------------------------------- 1 | from apps.project.handlers import * 2 | from dtlib import filetool 3 | 4 | app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 5 | 6 | url = [ 7 | 8 | (r"/%s/create-test-project/" % app_path, CreateTestProject), # 创建项目 9 | (r"/%s/update-test-project/" % app_path, UpdateTestProject), # 更新项目 10 | (r"/%s/delete-test-project/" % app_path, DeleteTestProject), # 删除项目 11 | (r"/%s/list-projects/" % app_path, ListProjects), # 本组织的所有项目 12 | 13 | (r"/%s/xtest-client-config/" % app_path, ListProjectsNote), # 根据project——id查找app-id 14 | 15 | (r"/%s/read-projects-record/" % app_path, ReadProjectsRecord), # 根据project—id查找30次的构建情况 16 | (r"/%s/get-tags/" % app_path, GetProjectTags), # 根据project—id查找30次的构建情况 17 | 18 | ] 19 | -------------------------------------------------------------------------------- /apps/share/__init__.py: -------------------------------------------------------------------------------- 1 | # 一些带传播性的功能放这里面 2 | -------------------------------------------------------------------------------- /apps/share/docs.py: -------------------------------------------------------------------------------- 1 | """ 2 | 分享link 3 | """ 4 | # from aiomotorengine import StringField, IntField 5 | # from dtlib.tornado.base_docs import OrgUserDataDocument 6 | # 7 | # from xt_base.document.base_docs import ProjectBaseDocument 8 | 9 | 10 | # todo 后面都要加上项目的名称的冗余 11 | # TODO: 后面有需要重写相关功能 12 | 13 | class ShareTestReport(OrgUserDataDocument, ProjectBaseDocument): 14 | """ 15 | 分享测试报告,只有拥有此share的ID流水号的,就可以访问此报告信息, 16 | 永久性的 17 | """ 18 | 19 | __collection__ = 'share_test_report' 20 | rep_id = StringField() # 报告的流水号 21 | stoken = StringField() # 访问此报告时所需要的token串号,share token 22 | cnt = IntField() # 被访问的次数 23 | mark = StringField() # 关于此分享的备注信息 24 | # project = ReferenceField(reference_document_type=Project) # 测试数据所属的项目 25 | # project_name = StringField() # 项目名称,冗余 26 | 27 | 28 | 29 | class ShareProjectReport(OrgUserDataDocument, ProjectBaseDocument): 30 | """ 31 | 分享测试项目报告,只有拥有此share的ID流水号的,就可以访问此报告信息, 32 | 永久性的 33 | """ 34 | __collection__ = 'share_project_report' 35 | pro_id = StringField() # 测试项目id 36 | stoken = StringField() # 访问此报告时所需要的token串号,share token 37 | cnt = IntField() # 被访问的次数 38 | mark = StringField() # 关于此分享的备注信息 39 | # project = ReferenceField(reference_document_type=Project) # 测试数据所属的项目 40 | # project_name = StringField() # 项目名称,冗余 -------------------------------------------------------------------------------- /apps/share/handlers.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | from pymongo import DESCENDING 4 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 5 | from bson import ObjectId 6 | from dtlib import jsontool 7 | from dtlib.randtool import generate_uuid_token 8 | from dtlib.tornado.decos import token_required 9 | from dtlib.utils import list_have_none_mem 10 | from dtlib.web.constcls import ConstData 11 | from dtlib.web.decos import deco_jsonp 12 | from dtlib.web.tools import get_std_json_response 13 | 14 | from dtlib.tornado.utils import get_org_data_paginator 15 | 16 | from dtlib.tornado.utils import set_default_rc_tag 17 | 18 | share_page = '/utest-report-share.html?stoken=' # 单独部署的一套前端内容 19 | 20 | pro_share_page = '/pro-report-share.html?stoken=' # 单独部署的一套前端内容 21 | 22 | 23 | class GetUtestShareLink(MyUserBaseHandler): 24 | """ 25 | 获取分享链接 26 | """ 27 | 28 | @token_required() 29 | @deco_jsonp() 30 | async def get(self): 31 | """ 32 | 1. 自己当前的组织下的资源 33 | 2. 生成share的内容 34 | 35 | 支持手机端的 36 | :return: 37 | """ 38 | 39 | rep_id = self.get_argument('rep_id', None) # 测试报告的ID 40 | 41 | if list_have_none_mem(*[rep_id]): 42 | return ConstData.msg_args_wrong 43 | 44 | db = self.get_async_mongo() 45 | share_col = db.share_test_report 46 | utest_col = db.unit_test_data 47 | proj_col = db.test_project 48 | 49 | # todo 做资源归属和权限的判断 50 | test_data = await utest_col.find_one({'_id': ObjectId(rep_id)}) 51 | project = await proj_col.find_one({'_id': test_data['pro_id']}) 52 | 53 | # if (project is None) or (project.organization is None): 54 | if project is None: 55 | return ConstData.msg_forbidden 56 | 57 | user_org = await self.get_organization() 58 | pro_org_id = project['organization'] 59 | if pro_org_id != user_org: 60 | return ConstData.msg_forbidden 61 | 62 | share_obj = await share_col.find_one({'rep_id': rep_id}) 63 | 64 | if share_obj is None: # 如果不存在分享Link则创建一组 65 | stoken = generate_uuid_token() # 生成一组随机串,分享的时候会看到 66 | 67 | share_data = dict( 68 | rep_id=rep_id, 69 | stoken=stoken, 70 | cnt=0, 71 | is_del=False, 72 | project=project['_id'], 73 | p_name=project['project_name'], 74 | owner=project['owner'], 75 | owner_name=project['owner_name'], 76 | organization=project['organization'], 77 | org_name=project['org_name'] 78 | ) 79 | # await share_obj.set_project_tag(project) 80 | # await share_obj.set_org_user_tag(http_req=self) 81 | share_data = set_default_rc_tag(share_data) 82 | await share_col.insert(share_data) 83 | share_url = share_page + stoken 84 | else: 85 | # 如果有,则直接使用 86 | share_url = share_page + share_obj['stoken'] 87 | 88 | res_dict = dict( 89 | share_url=share_url 90 | ) 91 | 92 | return get_std_json_response(data=jsontool.dumps(res_dict)) 93 | 94 | 95 | class GetUtestShareData(MyUserBaseHandler): 96 | """ 97 | 获取分享的单元测试数据接口 98 | """ 99 | 100 | @deco_jsonp() 101 | async def get(self): 102 | stoken = self.get_argument('stoken', None) 103 | 104 | if list_have_none_mem(*[stoken]): 105 | return ConstData.msg_args_wrong 106 | 107 | db = self.get_async_mongo() 108 | share_col = db.share_test_report 109 | utest_col = db['unit_test_data'] 110 | share_obj = await share_col.find_one({'stoken': stoken}) 111 | 112 | if share_obj is None: 113 | return ConstData.msg_forbidden 114 | 115 | # todo 把完整的Unittest的报告内容获取出来并返回 116 | 117 | msg_details = utest_col.find({"_id": ObjectId(str(share_obj['rep_id']))}) 118 | msg_content_list = await msg_details.to_list(1) 119 | 120 | msg_content = msg_content_list[0] 121 | 122 | # 做一个阅读访问次数的计数 123 | cnt = share_obj['cnt'] 124 | if cnt is None: 125 | cnt = 0 126 | cnt += 1 127 | await share_col.update({'stoken': stoken}, {'$set':{'cnt': cnt}}) 128 | 129 | return get_std_json_response(data=jsontool.dumps(msg_content, ensure_ascii=False)) 130 | 131 | 132 | class GetProjectShareLink(MyUserBaseHandler): 133 | """ 134 | 获取项目成长分享链接 135 | """ 136 | 137 | @token_required() 138 | @deco_jsonp() 139 | async def get(self): 140 | """ 141 | 1. 自己当前的组织下的资源 142 | 2. 生成share的内容 143 | 144 | 支持手机端的 145 | :return: 146 | """ 147 | 148 | pro_id = self.get_argument('project_id', None) # 测试报项目的ID 149 | tag = self.get_argument('tag', 'default') # 测试报项目的ID 150 | 151 | if list_have_none_mem(*[pro_id]): 152 | return ConstData.msg_args_wrong 153 | 154 | # todo 做资源归属和权限的判断 155 | 156 | user_org = await self.get_organization() 157 | 158 | db = self.get_async_mongo() 159 | proj_col = db.test_project 160 | share_col = db.share_project_report 161 | 162 | project = await proj_col.find_one({'_id': ObjectId(str(pro_id))}) 163 | 164 | if tag != 'default' and tag not in project['tags']: 165 | return ConstData.msg_args_wrong 166 | 167 | pro_org_id = project['organization'] 168 | 169 | if (project is None) or (pro_org_id is None): 170 | return ConstData.msg_forbidden 171 | 172 | if pro_org_id != user_org: 173 | return ConstData.msg_forbidden 174 | 175 | #todo: delete default next release 176 | condition = { 177 | 'pro_id': pro_id 178 | } 179 | if tag == 'default': 180 | condition['$or'] = [ 181 | {'tag': 'default'}, 182 | {'tag': {'$exists': False}} 183 | ] 184 | else: 185 | condition['tag'] = tag 186 | 187 | share_obj = await share_col.find_one(condition) 188 | 189 | if share_obj is None: # 如果不存在分享Link则创建一组 190 | stoken = generate_uuid_token() # 生成一组随机串,分享的时候会看到 191 | 192 | share_data = dict( 193 | pro_id=pro_id, 194 | stoken=stoken, 195 | cnt=0, 196 | project=project['_id'], 197 | p_name = project['project_name'], 198 | owner = project['owner'], 199 | owner_name = project['owner_name'], 200 | organization = project['organization'], 201 | org_name = project['org_name'], 202 | tag=tag 203 | ) 204 | share_data = set_default_rc_tag(share_data) 205 | await share_col.insert(share_data) 206 | share_url = pro_share_page + stoken 207 | else: 208 | # 如果有,则直接使用 209 | share_url = pro_share_page + share_obj['stoken'] 210 | 211 | res_dict = dict( 212 | share_url=share_url 213 | ) 214 | 215 | return get_std_json_response(data=jsontool.dumps(res_dict)) 216 | 217 | 218 | class GetProjectShareData(MyUserBaseHandler): 219 | """ 220 | 获取分享的单元测试数据接口 221 | """ 222 | 223 | @deco_jsonp() 224 | async def get(self): 225 | stoken = self.get_argument('stoken', None) 226 | if list_have_none_mem(*[stoken]): 227 | return ConstData.msg_args_wrong 228 | 229 | page_size = self.get_argument('page_size', 30) 230 | page_idx = self.get_argument('page_idx', 1) 231 | page_size = int(page_size) 232 | page_idx = int(page_idx) 233 | 234 | db = self.get_async_mongo() 235 | share_col = db.share_project_report 236 | utest_col = db['unit_test_data'] 237 | 238 | share_obj = await share_col.find_one({'stoken': stoken}) 239 | 240 | if share_obj is None: 241 | return ConstData.msg_forbidden 242 | 243 | # 做一个阅读访问次数的计数 244 | await share_col.update({'stoken': stoken}, {'$inc':{'cnt': 1}}) 245 | 246 | condition = { 247 | "pro_id": share_obj['project'], 248 | "is_del": False 249 | } 250 | #todo: delete default next release 251 | if 'tag' in share_obj.keys(): 252 | tag = share_obj['tag'] 253 | if tag != 'default': 254 | condition['tag'] = tag 255 | else: 256 | condition['$or'] = [ 257 | {'tag': tag}, {'tag': {'$exists': False}} 258 | ] 259 | else: 260 | condition['$or'] = [ 261 | {'tag': 'default'}, {'tag': {'$exists': False}} 262 | ] 263 | 264 | res = utest_col.find( 265 | condition, {"details": 0}, 266 | sort=[('rc_time', DESCENDING)]) 267 | 268 | page_count = await res.count() 269 | msg_details = res.skip(page_size * (page_idx - 1)).limit(page_size) # 进行分页 270 | 271 | total_page = math.ceil(page_count / page_size) # 总的页面数 272 | 273 | msg_content_list = await msg_details.to_list(page_size) 274 | 275 | page_res = dict( 276 | page_idx=page_idx, 277 | page_total_cnts=total_page, 278 | page_cap=page_size, 279 | page_data=msg_content_list 280 | ) 281 | 282 | return get_std_json_response(data=jsontool.dumps(page_res, ensure_ascii=False)) 283 | 284 | 285 | class MyUtestShare(MyUserBaseHandler): 286 | @token_required() 287 | @deco_jsonp() 288 | async def get(self): 289 | """ 290 | 获取当前用户下的所有的utest分享链接 291 | :return: 292 | """ 293 | return await get_org_data_paginator(self, col_name='share_test_report') 294 | 295 | 296 | class MyProjectShare(MyUserBaseHandler): 297 | @token_required() 298 | @deco_jsonp() 299 | async def get(self): 300 | """ 301 | 获取当前用户下的所有的utest分享链接 302 | :return: 303 | """ 304 | return await get_org_data_paginator(self, col_name='share_project_report') 305 | 306 | 307 | class UpdateUtestShare(MyUserBaseHandler): 308 | @token_required() 309 | @deco_jsonp() 310 | async def post(self): 311 | """ 312 | 删除分享链接,直接完全删除,不需要留备份 313 | :return: 314 | """ 315 | post_dict = self.get_post_body_dict() 316 | share_id = post_dict.get('share_id', None) 317 | mark = post_dict.get('mark', None) 318 | if list_have_none_mem(*[share_id, mark]): 319 | return ConstData.msg_args_wrong 320 | 321 | user = self.get_current_session_user() 322 | db = self.get_async_mongo() 323 | user_col = db.g_users 324 | share_col = db.share_test_report 325 | user_result = await user_col.find_one({'_id': ObjectId(user)}) 326 | res = await share_col.find_one({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}) 327 | 328 | if res is None: 329 | return ConstData.msg_forbidden 330 | 331 | await share_col.update({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}, 332 | {'$set': {'mark': mark}}) 333 | return ConstData.msg_succeed 334 | 335 | 336 | class DeleteUtestShare(MyUserBaseHandler): 337 | @token_required() 338 | @deco_jsonp() 339 | async def get(self): 340 | """ 341 | 删除分享链接,直接完全删除,不需要留备份 342 | :return: 343 | """ 344 | share_id = self.get_argument('share_id', None) 345 | if list_have_none_mem(*[share_id]): 346 | return ConstData.msg_args_wrong 347 | user = self.get_current_session_user() 348 | 349 | db = self.get_async_mongo() 350 | user_col = db.g_users 351 | share_col = db.share_test_report 352 | 353 | user_result = await user_col.find_one({'_id': ObjectId(user)}) 354 | 355 | res = await share_col.find_one({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}) 356 | """:type:ShareTestReport""" 357 | if res is None: 358 | return ConstData.msg_forbidden 359 | await share_col.remove({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}) 360 | return ConstData.msg_succeed 361 | 362 | 363 | class UpdateProjectShare(MyUserBaseHandler): 364 | @token_required() 365 | @deco_jsonp() 366 | async def post(self): 367 | """ 368 | 删除分享链接 369 | :return: 370 | """ 371 | post_dict = self.get_post_body_dict() 372 | share_id = post_dict.get('share_id', None) 373 | mark = post_dict.get('mark', None) 374 | if list_have_none_mem(*[share_id, mark]): 375 | return ConstData.msg_args_wrong 376 | 377 | user = self.get_current_session_user() 378 | db = self.get_async_mongo() 379 | user_col = db.g_users 380 | share_col = db.share_project_report 381 | user_result = await user_col.find_one({'_id': ObjectId(user)}) 382 | res = await share_col.find_one({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}) 383 | 384 | if res is None: 385 | return ConstData.msg_forbidden 386 | await share_col.update({'_id': ObjectId(str(share_id)), 'owner': user_result['_id']}, 387 | {'$set': {'mark': mark}}) 388 | return ConstData.msg_succeed 389 | 390 | 391 | class DeleteProjectShare(MyUserBaseHandler): 392 | @token_required() 393 | @deco_jsonp() 394 | async def get(self): 395 | """ 396 | 删除分享链接 397 | :return: 398 | """ 399 | share_id = self.get_argument('share_id', None) 400 | if list_have_none_mem(*[share_id]): 401 | return ConstData.msg_args_wrong 402 | user = self.get_current_session_user() 403 | 404 | db = self.get_async_mongo() 405 | share_proj_col = db.share_project_report 406 | 407 | res = await share_proj_col.find_one({'_id': ObjectId(str(share_id)), 'owner': ObjectId(user)}) 408 | 409 | if res is None: 410 | return ConstData.msg_forbidden 411 | 412 | await share_proj_col.remove({'_id': ObjectId(str(share_id)), 'owner': ObjectId(user)}) 413 | return ConstData.msg_succeed 414 | -------------------------------------------------------------------------------- /apps/share/urls.py: -------------------------------------------------------------------------------- 1 | from dtlib import filetool 2 | 3 | from apps.share.handlers import * 4 | 5 | app_path = filetool.get_parent_folder_name(__file__) # 'apis' # set the relative path in one place 6 | 7 | url = [ 8 | # 分享测试报告 9 | (r"/%s/get-utest-share-link/" % app_path, GetUtestShareLink), # 获取Utest的分享链接 10 | (r"/%s/get-pro-share-link/" % app_path, GetProjectShareLink), # 获取project的分享链接 11 | 12 | # 对分享的外链进行管理 13 | (r"/%s/my-utest-share/" % app_path, MyUtestShare), # 我的Utest的分享链接 14 | (r"/%s/delete-utest-share/" % app_path, DeleteUtestShare), 15 | (r"/%s/update-utest-share/" % app_path, UpdateUtestShare), 16 | 17 | (r"/%s/my-project-share/" % app_path, MyProjectShare), # 我的试项目报告的数据 18 | (r"/%s/update-project-share/" % app_path, UpdateProjectShare), 19 | (r"/%s/delete-project-share/" % app_path, DeleteProjectShare), # 20 | 21 | # 通过stoken来获取数据的接口,不同的认证方式 22 | # todo 这块可以分离部署 23 | (r"/%s/get-utest-share-data/" % app_path, GetUtestShareData), # 以分享途径获得测试报告的数据 24 | (r"/%s/get-pro-share-data/" % app_path, GetProjectShareData), # 以分享途径获得测试项目报告的数据 25 | 26 | ] 27 | -------------------------------------------------------------------------------- /apps/testdata/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/testdata/__init__.py -------------------------------------------------------------------------------- /apps/testdata/docs.py: -------------------------------------------------------------------------------- 1 | # from aiomotorengine import StringField, IntField, BooleanField, FloatField, DateTimeField, ReferenceField, ListField 2 | # 3 | # from xt_base.document.source_docs import InfoAsset 4 | # from xt_base.document.base_docs import Project 5 | # from dtlib.aio.base_mongo import MyDocument 6 | # from dtlib.tornado.account_docs import Organization 7 | # from dtlib.tornado.base_docs import OrgDataBaseDocument 8 | 9 | # TODO: 后面有需要重写相关功能 10 | 11 | class ProjectDataDocument(OrgDataBaseDocument, MyDocument): 12 | """ 13 | 项目数据 14 | """ 15 | # 数据归属 16 | project = ReferenceField(reference_document_type=Project) # 测试数据所属的项目 17 | project_name = StringField() # 项目名称,冗余 18 | 19 | async def set_project_tag(self, project): 20 | """ 21 | 打上项目标记 22 | :type project:Project 23 | :return: 24 | """ 25 | 26 | organization = project.organization 27 | self.project = project 28 | self.project_name = project.project_name 29 | self.organization = organization 30 | self.org_name = organization.name 31 | 32 | 33 | class ProjectFeedBack(ProjectDataDocument): 34 | """ 35 | 项目相关的问题反馈 36 | """ 37 | 38 | msg = StringField() 39 | # images = ListField() # 图片列表 40 | # todo 语音和视频 41 | label = StringField() # 关于问题的标记 42 | status = IntField() # 处理的状态码 43 | 44 | 45 | class ServiceStatus(MyDocument): 46 | """ 47 | 服务器开放的服务状态 48 | """ 49 | __collection__ = "service_status" 50 | 51 | info_asset = ReferenceField(reference_document_type=InfoAsset) # 所属资产,信息资产 52 | port = IntField() # 端口号 53 | protocol = StringField() # 协议名称 54 | status = StringField() # 状态 55 | version = StringField # 版本 56 | 57 | 58 | class ApiTestData(ProjectDataDocument): 59 | """ 60 | 测试数据统计表---因为性能问题,取消使用 2016-09-30 61 | """ 62 | __collection__ = "api_test_data" 63 | 64 | was_successful = BooleanField() # 是否是成功的 65 | total = IntField() 66 | failures = IntField() 67 | errors = IntField() 68 | skipped = IntField() 69 | run_time = StringField(max_length=1024) 70 | 71 | 72 | class ApiTestDataNote(MyDocument): 73 | """ 74 | api测试详细数据,只记录失败和错误---因为性能问题,取消使用 2016-09-30 75 | """ 76 | __collection__ = "api_test_data_note" 77 | apitestdata_id = StringField(max_length=1024) 78 | test_case = StringField(max_length=1024) # 出错测试用例 79 | explain = StringField(max_length=1024) # 目的 80 | status = StringField(max_length=1024) # 测试状态,失败或者错误 81 | note = StringField() # 详细记录 82 | 83 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 84 | 85 | 86 | class UnitTestDataDetail(MyDocument): 87 | """ 88 | api测试详细数据,只记录失败和错误--插入时没有使用 89 | """ 90 | test_case = StringField(max_length=1024) # 出错测试用例 91 | explain = StringField(max_length=1024) # 目的 92 | status = StringField(max_length=1024) # 测试状态,失败或者错误 93 | note = StringField() # 详细记录 94 | 95 | 96 | class UnitTestData(MyDocument): 97 | """ 98 | 测试数据统计表--插入时没有使用,没有使用ORM 99 | """ 100 | __collection__ = "unit_test_data" 101 | 102 | pro_version = StringField(max_length=1024) # 项目版本号:1.2.3.4 103 | was_successful = BooleanField() # 是否是成功的 104 | total = IntField() 105 | failures = IntField() 106 | errors = IntField() 107 | skipped = IntField() 108 | run_time = FloatField() 109 | details = ListField(ReferenceField(UnitTestDataDetail)) 110 | 111 | # 数据归属 112 | project = ReferenceField(reference_document_type=Project) # 测试数据所属的项目 113 | project_name = StringField() # 项目名称,冗余 114 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 115 | 116 | 117 | class PerformReport(MyDocument): 118 | """ 119 | 性能测试报告,已经作废,在bench中有专门的处理的了:2016-09-10 120 | waste 121 | """ 122 | __collection__ = "perform_report" 123 | 124 | server_soft_ware = StringField(max_length=2048) 125 | server_host_name = StringField(max_length=2048) 126 | server_port = StringField(max_length=64) 127 | doc_path = StringField(max_length=2048) 128 | doc_length = IntField() # 文档长度,bytes 129 | con_level = IntField() # 并发量 130 | test_time_taken = FloatField() # 消耗时间 seconds 131 | complete_req = IntField() # 完成请求数 132 | failed_req = IntField() # 失败请求数 133 | non_2xx_res = IntField() # 非2xx请求数 134 | total_trans = IntField() # 总传输数据量bytes 135 | html_trans = IntField() # 总html传输数据量bytes 136 | req_per_sec = FloatField() # 每秒请求量 137 | time_per_req = FloatField() # 平均http请求响应时间:ms 138 | time_per_req_across = FloatField() # 所有并发请求量耗时(平均事务响应时间):ms 139 | trans_rate = FloatField() # 每秒传输数据量,Kbytes/sec 140 | 141 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 142 | 143 | 144 | class SafetyTestReport(ProjectDataDocument): 145 | """ 146 | 安全测试报告 147 | """ 148 | __collection__ = "safety_test_report" 149 | # project_name = StringField() # 项目名,里面加上版本号,这个并不是具体的内部项目,和其它几种测试数据不同 150 | hack_tool = StringField() # 用于hack的软件名称 151 | total_cnts = IntField() # 总计次数 152 | success_cnts = IntField() # 成功次数 153 | success_rate = FloatField() # 成功率,冗余 154 | time_cycle = FloatField() # 花费时间:s 155 | crack_rate = FloatField() # 破解效率,冗余 156 | mark = StringField() # 描述备注 157 | # organization = ReferenceField(reference_document_type=Organization) # 组织 158 | 159 | 160 | class ApiReqDelay(MyDocument): 161 | """测试接口的延时 162 | """ 163 | __collection__ = "api_req_delay" 164 | 165 | domain = StringField(max_length=2048) # ms,基准域 166 | path = StringField(max_length=2048) # ms,接口路径及参数 167 | delay = FloatField() # ms,请求时间 168 | http_status = IntField() # http状态值 169 | 170 | # 数据归属 171 | project = ReferenceField(reference_document_type=Project) 172 | project_name = StringField() # 项目名称,冗余 173 | organization = ReferenceField(reference_document_type=Organization) 174 | 175 | 176 | class ExDataRecord(MyDocument): 177 | """ 178 | 实验数据的记录表 179 | """ 180 | 181 | __collection__ = "ex_data_record" 182 | 183 | # --------------数据状态值记录----------- 184 | record_status = StringField(max_length=64) # 数据的状态:reported,filted,experiment 185 | # --------------数据发现和登记期----------- 186 | custom_name = StringField(max_length=256) # 用户名称 187 | captcha_id = StringField(max_length=64) 188 | event_start_time = DateTimeField() # weblog产生开始时间 189 | event_end_time = DateTimeField() # weblog产生结束时间 190 | event_reporter = StringField(max_length=256) # 事件汇报人 191 | event_report_time = DateTimeField() # 事件汇报时间 192 | track_class = StringField(max_length=64) 193 | js_version = StringField(max_length=16) 194 | js_tag_page = StringField(max_length=2048) # 在bitbucket或者tower中的当前版本的修改标记 195 | css_version = StringField(max_length=16) 196 | css_tag_page = StringField(max_length=2048) # 在bitbucket或者tower中的当前版本的修改标记 197 | # --------------数据过滤的采集备案期----------- 198 | data_collection_name = StringField(max_length=256) 199 | producers = StringField(max_length=256) 200 | ex_user = StringField(max_length=256) # 数据收集人 201 | action_time = DateTimeField() # 数据备案时间 202 | # event_time = DateTimeField() 203 | file_name = StringField(max_length=64) 204 | file_path = StringField(max_length=2048) # 实验数据文件所在路径,以FTP的方式来共享 205 | file_size = IntField() 206 | record_cnt = IntField() # 记录的数目 207 | # --------------数据实验期----------- 208 | researcher = StringField(max_length=256) # 数据实验人 209 | researche_time = DateTimeField() # 数据实验时间 210 | research_result = StringField(max_length=10240) # 验证处理结果 211 | # experiment_id= Refer 212 | 213 | 214 | class CurtainExData(MyDocument): 215 | """ 216 | Curtain项目的测试数据 217 | """ 218 | # answer = ListField() 219 | # track_data = ListField() 220 | action_user = StringField(max_length=256) 221 | 222 | 223 | class LimitTestData(MyDocument): 224 | """ 225 | 测试数据统计表 226 | """ 227 | __collection__ = "limit_test_data" 228 | 229 | # id = ObjectId() 230 | was_successful = BooleanField() # 是否是成功的 231 | total = IntField() 232 | failures = IntField() 233 | errors = IntField() 234 | skipped = IntField() 235 | run_time = StringField(max_length=1024) 236 | 237 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 238 | 239 | 240 | class LimitTestDataNote(MyDocument): 241 | """ 242 | api测试详细数据,只记录失败和错误(作废-2016-11-23) 243 | """ 244 | __collection__ = "limit_test_data_note" 245 | limittestdata_id = StringField(max_length=1024) 246 | test_case = StringField(max_length=1024) # 出错测试用例 247 | explain = StringField(max_length=1024) # 目的 248 | status = StringField(max_length=1024) # 测试状态,失败或者错误 249 | note = StringField() # 详细记录 250 | 251 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 252 | 253 | 254 | class UnitPenetrationTest(MyDocument): 255 | """ 256 | 渗透测试详细信息 257 | """ 258 | test_case = StringField(max_length=1024) # 测试目的 259 | result = StringField(max_length=1024) # 结果 260 | 261 | 262 | class PenetrationTestData(ProjectDataDocument): 263 | """ 264 | 渗透测试详情 265 | """ 266 | __collection__ = "penetration_test_data" 267 | start_time = StringField() 268 | use_time = FloatField() 269 | note = StringField() 270 | # project = ReferenceField(reference_document_type=Project) # 测试数据所属的项目 271 | # project_name = StringField() # 项目名称,冗余 272 | # organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 273 | 274 | 275 | class PenetrationTestDataNote(MyDocument): 276 | """ 277 | 渗透测试详情 278 | """ 279 | __collection__ = "penetration_test_data_note" 280 | penetration_id = StringField(max_length=1024) 281 | ip = StringField(max_length=1024) 282 | details = ListField(ReferenceField(UnitPenetrationTest)) 283 | # SSHRootEmptyPassword = StringField(max_length=1024) 284 | # RedisEmptyPassword = StringField(max_length=1024) 285 | # MongoEmptyPassword = StringField(max_length=1024) 286 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 287 | 288 | 289 | class ProxyipTestData(MyDocument): 290 | """ 291 | 爆破测试 292 | """ 293 | __collection__ = "proxyip_test_data" 294 | remoteip = StringField(max_length=1024) 295 | originalip = StringField(max_length=1024) 296 | proxyip = StringField(max_length=1024) 297 | 298 | organization = ReferenceField(reference_document_type=Organization) # 数据所属的组织 299 | 300 | 301 | class FeedbackMsg(MyDocument): 302 | """ 303 | feedback 304 | """ 305 | 306 | __collection__ = 'feedback_msg' 307 | file_path = StringField() # 文件路径 308 | msg = StringField() # msg 309 | -------------------------------------------------------------------------------- /apps/testdata/handlers.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 4 | from bson import ObjectId 5 | from dtlib import jsontool 6 | from dtlib.aio.base_mongo import wrap_default_rc_tag 7 | from dtlib.aio.decos import my_async_paginator, my_async_jsonp 8 | from dtlib.dtlog import dlog 9 | from dtlib.tornado.base_hanlder import MyAppBaseHandler 10 | from dtlib.tornado.decos import token_required, app_token_required 11 | from dtlib.utils import list_have_none_mem 12 | from dtlib.web.constcls import ConstData, ResCode 13 | from dtlib.web.decos import deco_jsonp 14 | from dtlib.web.tools import get_std_json_response 15 | from dtlib.web.valuedict import ClientTypeDict 16 | from dtlib.tornado.utils import get_org_data 17 | from dtlib.tornado.utils import wrap_org_tag, wrap_project_tag, get_org_data_paginator 18 | 19 | 20 | class ListApiTestData(MyUserBaseHandler): 21 | """ 22 | 前段页面显示出数据 23 | 如果没有任何参数,则默认返回首页的n条数据 24 | 如果并非首次回返 25 | --因为性能问题,取消使用 2016-09-30 26 | """ 27 | 28 | @token_required() 29 | @my_async_jsonp 30 | @my_async_paginator 31 | async def get(self): 32 | pro_id = self.get_argument('pro_id', None) 33 | if pro_id is None: 34 | return await get_org_data(self, collection='api_test_data') 35 | else: 36 | return await get_org_data(self, collection='api_test_data', pro_id=pro_id) 37 | 38 | 39 | class ListSafetyTestData(MyUserBaseHandler): 40 | """ 41 | """ 42 | 43 | @token_required() 44 | @my_async_jsonp 45 | @my_async_paginator 46 | async def get(self): 47 | return await get_org_data(self, collection='safety_test_report') 48 | 49 | 50 | class ListApiReqDelay(MyUserBaseHandler): 51 | @token_required() 52 | @my_async_jsonp 53 | @my_async_paginator 54 | async def get(self): 55 | """ 56 | 查询所有的接口延时测试信息 57 | :return: 58 | """ 59 | pro_id = self.get_argument('pro_id', None) 60 | if pro_id is None: 61 | return await get_org_data(self, collection='api_req_delay') 62 | else: 63 | return await get_org_data(self, collection='api_req_delay', pro_id=pro_id) 64 | # return await ApiReqDelay.objects.filter(project_name=project_name).order_by('rc_time', 65 | # direction=DESCENDING).find_all() 66 | 67 | # return ApiReqDelay.objects.order_by('rc_time', direction=DESCENDING).find_all() 68 | 69 | 70 | class ListPerformanceTestData(MyUserBaseHandler): 71 | """ 72 | 废弃掉的功能 73 | """ 74 | 75 | @token_required() 76 | @my_async_jsonp 77 | @my_async_paginator 78 | async def get(self): 79 | return await get_org_data(self, collection='perform_report') 80 | 81 | 82 | class ListUnitTestData(MyUserBaseHandler): 83 | """ 84 | 带分页的内容,不使用ORM了,这样返回会快速很多 85 | """ 86 | 87 | @token_required() 88 | @my_async_jsonp 89 | async def get(self): 90 | """ 91 | 客户端将整个报告上传到服务器 92 | :return:返回插入的数据的id,方便后续做关联插入 93 | """ 94 | pro_id = self.get_argument('pro_id', None) 95 | tag = self.get_arguments('tag') 96 | return await get_org_data_paginator(self, col_name='unit_test_data', pro_id=pro_id, hide_fields={'details': 0}, tag=tag) 97 | 98 | 99 | class DeleteTestData(MyUserBaseHandler): 100 | """ 101 | 删除testdata(实际是将is_del设置为true) 102 | """ 103 | 104 | @token_required() 105 | @my_async_jsonp 106 | async def get(self): 107 | id = self.get_argument('id', None) 108 | if id is None: 109 | return ConstData.msg_args_wrong 110 | 111 | # todo 做资源归属和权限的判断 112 | # project_obj = await .objects.get(id=ObjectId(str(id))) 113 | mongo_coon = self.get_async_mongo() 114 | mycol = mongo_coon['unit_test_data'] 115 | pro_col = mongo_coon['test_project'] 116 | testdata = await mycol.find_one({'_id': ObjectId(str(id))}) 117 | testdata_organization = testdata['organization'] 118 | user_org = await self.get_organization() 119 | 120 | if (testdata is None) or (testdata_organization is None): 121 | return ConstData.msg_forbidden 122 | 123 | pro_org_id = testdata_organization 124 | if pro_org_id != user_org: 125 | return ConstData.msg_forbidden 126 | 127 | await mycol.update({"_id": ObjectId(str(id))}, {"$set": {"is_del": True}}) 128 | 129 | if 'tag' in testdata.keys(): 130 | pro_id = testdata['pro_id'] 131 | tag = testdata['tag'] 132 | # todo: count records of this tag, if < 1, delete tag in project 133 | tags = await mycol.find({"pro_id": ObjectId(pro_id), 'is_del': False, 'tag': tag}, {'tag': 1}).count() 134 | if tags < 1: 135 | project = await pro_col.find_one({"_id": ObjectId(pro_id)}) 136 | proj_tags = project['tags'] 137 | proj_tags.remove(tag) 138 | await pro_col.update({"_id": ObjectId(pro_id)}, {"$set": {"tags": proj_tags}}) 139 | return ConstData.msg_succeed 140 | 141 | 142 | class CreateUnitTestData(MyAppBaseHandler): 143 | """ 144 | 插入测试数据,包括总值和明细值,高性能的一次性插入,没有用ORM 145 | """ 146 | 147 | @app_token_required() 148 | # @my_async_jsonp 149 | async def post(self): 150 | """ 151 | 客户端将整个报告上传到服务器 152 | :return: 153 | """ 154 | token = self.get_argument('token', None) 155 | req_dict = self.get_post_body_dict() 156 | 157 | # 做好字段的检查工作 158 | pro_id = req_dict.get('pro_id', None) 159 | failures = req_dict.get('failures', None) 160 | errors = req_dict.get('errors', None) 161 | details = req_dict.get('details', None) 162 | skipped = req_dict.get('skipped', None) 163 | pro_version = req_dict.get('pro_version', None) 164 | run_time = req_dict.get('run_time', None) 165 | total = req_dict.get('total', None) 166 | was_successful = req_dict.get('was_successful', None) 167 | tag = req_dict.get('tag', 'default') 168 | 169 | if list_have_none_mem( 170 | *[pro_id, failures, errors, 171 | details, skipped, 172 | pro_version, run_time, 173 | total, was_successful, ]): 174 | return ConstData.msg_args_wrong 175 | 176 | if len(pro_version) > 32: # 如果版本号长度大于32,比如出现了没有标定版本号的情况 177 | pro_version = '0.0.0.0.0' 178 | req_dict['pro_version'] = pro_version 179 | 180 | db = self.get_async_mongo() 181 | proj_col = db.test_project 182 | test_data_col = db.unit_test_data 183 | 184 | # project = await Project.objects.get(id=ObjectId(pro_id)) 185 | project = await proj_col.find_one({'_id': ObjectId(pro_id)}) 186 | """:type:Project""" 187 | 188 | app_org = await self.get_organization() 189 | """:type:Organization""" 190 | 191 | # if (project is None) or (project.organization is None): 192 | if project is None: 193 | return ConstData.msg_forbidden 194 | 195 | # 权限鉴定,不允许越权访问别人的组织的项目 196 | pro_org_id = project['organization'] 197 | if pro_org_id != app_org: 198 | return ConstData.msg_forbidden 199 | 200 | # todo 后续的一些更细节的字段内在的约束检查 201 | 202 | req_dict = wrap_project_tag(req_dict, project) # 加上项目标签 203 | req_dict = wrap_default_rc_tag(req_dict) # 加上默认的标签 204 | req_dict = wrap_org_tag(req_dict, str(pro_org_id)) # 加上组织的标签 205 | insert_res = await test_data_col.insert(req_dict) 206 | if 'tags' in project.keys(): 207 | pro_tags = project['tags'] 208 | # if tag in pro_tags: 209 | # return ConstData.res_tpl % (ResCode.ok, 'success', '"' + str(insert_res) + '"') 210 | else: 211 | pro_tags = [] 212 | if tag not in pro_tags: 213 | pro_tags.append(tag) 214 | await proj_col.update({'_id': ObjectId(pro_id)}, {'$set': {'tags': pro_tags}}) 215 | # return ConstData.res_tpl % (ResCode.ok, 'success', '"' + str(insert_res) + '"') 216 | self.redirect('/share/get-utest-share-link/?token={}&rep_id={}'.format(token, insert_res)) 217 | 218 | 219 | class GetOneUnitTestData(MyUserBaseHandler): 220 | """ 221 | 获取一条记录 222 | """ 223 | 224 | @token_required() 225 | @my_async_jsonp 226 | async def get(self): 227 | """ 228 | 要做鉴权和区分 229 | :return: 230 | """ 231 | data_id = self.get_argument('id', None) 232 | if list_have_none_mem(*[data_id]): 233 | return ConstData.msg_args_wrong 234 | 235 | mongo_coon = self.get_async_mongo() 236 | mycol = mongo_coon['unit_test_data'] 237 | 238 | user_org = await self.get_organization() 239 | """:type:Organization""" 240 | 241 | msg_details = await mycol.find_one({ 242 | "organization": ObjectId(user_org), 243 | "_id": ObjectId(data_id) 244 | }) 245 | 246 | return get_std_json_response(data=jsontool.dumps(msg_details, ensure_ascii=False)) 247 | 248 | class ApiAuth(MyAppBaseHandler): 249 | """ 250 | 验证API是否被授权了,通过ID和KEY来换取token 251 | """ 252 | 253 | @my_async_jsonp 254 | async def post(self): 255 | appid = self.get_argument('appid_form', None) 256 | appkey = self.get_argument('appkey_form', None) 257 | 258 | if list_have_none_mem(*[appid, appkey]): 259 | return ConstData.msg_args_wrong 260 | 261 | db = self.get_async_mongo() 262 | app_col = db.test_data_app 263 | 264 | test_data_app = await app_col.find_one({'app_id': str(appid)}) # 后面要为app_id建立index 265 | if test_data_app is None: 266 | return ConstData.msg_none 267 | 268 | # 可以和数据库连接形成动态的验证 269 | if str(appkey) == str(test_data_app['app_key']): 270 | # todo:后面对于自动化的工具应用,要隔离部署,单独做一套体系,先默认使用某个人的信息了 271 | 272 | app_session = await self.create_app_session(app_id=appid, client_type=ClientTypeDict.api) 273 | 274 | if app_session is None: 275 | return ConstData.msg_forbidden 276 | 277 | # todo 后面要做高频的api接口的统计系统 278 | 279 | res_data = jsontool.dumps(app_session) 280 | dlog.debug(res_data) 281 | return get_std_json_response(data=res_data) 282 | else: 283 | return ConstData.msg_fail 284 | 285 | 286 | class ApiAuthout(MyAppBaseHandler): 287 | """ 288 | 取消API和认证 289 | """ 290 | 291 | @token_required() 292 | @deco_jsonp(is_async=False) 293 | def get(self): 294 | self.log_out() 295 | return get_std_json_response(code=200, msg='logout', data='') 296 | 297 | 298 | # code trash (2018-04-23 yx) 299 | # class CreatePerformReport(MyUserBaseHandler): 300 | # # TODO: change to motor 301 | # """ 302 | # 保存性能测试的数据指标 303 | # """ 304 | # 305 | # @token_required() 306 | # @my_async_jsonp 307 | # async def post(self): 308 | # # TODO 读取动态的内容 309 | # perform_report = PerformReport() 310 | # # data = json_decode(self.request.body) 311 | # 312 | # # perform_report.server_soft_ware='squid/3.4.2' 313 | # perform_report.server_host_name = self.get_argument('doc_path', None) 314 | # perform_report.server_port = '80' 315 | # # perform_report.doc_path = '/' 316 | # # perform_report.doc_length = 131 317 | # perform_report.con_level = self.get_argument('con_level', None) # 并发量 318 | # # perform_report.test_time_taken = 52.732 # 消耗时间 seconds 319 | # # perform_report.complete_req = data['con_level'] * 20 # 完成请求数 320 | # perform_report.failed_req = self.get_argument('failed_req', None) # 失败请求数 321 | # # perform_report.non_2xx_res = IntField()#非2xx请求数 322 | # # perform_report.total_trans = 62505778#总传输数据量bytes 323 | # # perform_report.html_trans = 62472673#总html传输数据量bytes 324 | # 325 | # 326 | # perform_report.req_per_sec = self.get_argument('req_per_sec', None) # 每秒请求量 327 | # perform_report.time_per_req = self.get_argument('time_per_req', None) # 平均http请求响应时间 328 | # perform_report.time_per_req_across = self.get_argument('time_per_req_across', None) # 平均事务响应时间 329 | # # perform_report.trans_rate = 1157.56#每秒传输数据量 330 | # # perform_report.time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') 331 | # perform_report.set_default_rc_tag() 332 | # 333 | # perform_report.organization = await self.get_organization() 334 | # 335 | # perform_report.save() 336 | # 337 | # return ConstData.msg_succeed 338 | # 339 | # 340 | # class CreateSafetyTestReport(MyUserBaseHandler): 341 | # # TODO: change to motor 342 | # """ 343 | # 保存安全测试数据 344 | # """ 345 | # 346 | # @app_token_required() 347 | # @my_async_jsonp 348 | # async def get(self): 349 | # project_id = self.get_argument('project_id', None) 350 | # total_cnts = self.get_argument('total_cnts', None) 351 | # success_cnts = self.get_argument('success_cnts', None) 352 | # time_cycle = self.get_argument('time_cycle', None) 353 | # # project_name = self.get_argument('project_name', None) 354 | # hack_tool = self.get_argument('hack_tool', None) 355 | # notes = self.get_argument('notes', None) 356 | # 357 | # if list_have_none_mem(*[project_id, total_cnts, success_cnts, time_cycle]) is True: 358 | # return ConstData.msg_args_wrong 359 | # 360 | # # todo 判定项目从属的问题,后面要做成专门的公共函数 361 | # app_org = await self.get_org_by_app() 362 | # """:type:Organization""" 363 | # project = await Project.objects.get(id=ObjectId(project_id)) 364 | # """:type:Project""" 365 | # if project is None or app_org is None: 366 | # return ConstData.msg_none 367 | # if app_org.get_id() != project.organization.get_id(): 368 | # return ConstData.msg_forbidden 369 | # 370 | # safety_report = SafetyTestReport() 371 | # safety_report.hack_tool = hack_tool 372 | # safety_report.total_cnts = int(total_cnts) 373 | # safety_report.success_cnts = int(success_cnts) 374 | # safety_report.success_rate = int(success_cnts) / int(total_cnts) 375 | # safety_report.time_cycle = float(time_cycle) 376 | # safety_report.crack_rate = float(time_cycle) / int(success_cnts) 377 | # 378 | # safety_report.mark = notes # 加上备注 379 | # safety_report.set_default_rc_tag() 380 | # await safety_report.set_project_tag(project) 381 | # await safety_report.save() 382 | # 383 | # return ConstData.msg_succeed 384 | # 385 | # 386 | # class CreateApiReqDelay(MyUserBaseHandler): 387 | # # TODO: change to motor 388 | # @app_token_required() 389 | # @my_async_jsonp 390 | # async def post(self): 391 | # """创建api访问的时间接口 392 | # """ 393 | # pro_id = self.get_argument('pro_id', None) 394 | # domain = self.get_argument('domain', None) 395 | # path = self.get_argument('path', None) 396 | # delay = self.get_argument('delay', None) 397 | # http_status = self.get_argument('http_status', None) 398 | # 399 | # if list_have_none_mem(*[domain, path, delay, pro_id, http_status]): 400 | # return ConstData.msg_args_wrong 401 | # 402 | # project = await Project.objects.get(id=ObjectId(pro_id)) 403 | # """:type:Project""" 404 | # if project is None: 405 | # return ConstData.msg_forbidden 406 | # 407 | # api_req_delay = ApiReqDelay() 408 | # api_req_delay.project = project 409 | # api_req_delay.project_name = project.project_name 410 | # api_req_delay.domain = domain 411 | # api_req_delay.delay = delay 412 | # api_req_delay.path = path 413 | # api_req_delay.http_status = http_status 414 | # api_req_delay.organization = await self.get_org_by_app() 415 | # api_req_delay.set_default_rc_tag() 416 | # 417 | # await api_req_delay.save() 418 | # 419 | # return ConstData.msg_succeed 420 | # 421 | # 422 | # class CreatePenetrationReport(MyUserBaseHandler): 423 | # # TODO: change to motor 424 | # """ 425 | # 渗透测试数据 426 | # """ 427 | # 428 | # @app_token_required() 429 | # @my_async_jsonp 430 | # async def post(self): 431 | # start_time = self.get_argument('start_time', None) 432 | # use_time = self.get_argument('use_time', None) 433 | # note = self.get_argument('note', None) 434 | # pro_id = self.get_argument('pro_id', None) 435 | # if list_have_none_mem(*[start_time, use_time]) is True: 436 | # return ConstData.msg_args_wrong 437 | # 438 | # project = await Project.objects.get(id=ObjectId(pro_id)) 439 | # """:type:Project""" 440 | # 441 | # app_org = await self.get_org_by_app() 442 | # """:type:Organization""" 443 | # 444 | # if (project is None) or (project.organization is None): 445 | # return ConstData.msg_forbidden 446 | # 447 | # # 权限鉴定,不允许越权访问别人的组织 448 | # if project.organization.get_id() != app_org.get_id(): 449 | # return ConstData.msg_forbidden 450 | # 451 | # penetration_report = PenetrationTestData() 452 | # penetration_report.start_time = start_time 453 | # penetration_report.use_time = use_time 454 | # penetration_report.note = note 455 | # penetration_report.project = project 456 | # penetration_report.project_name = project.project_name 457 | # penetration_report.organization = project.organization 458 | # penetration_report.set_default_rc_tag() 459 | # 460 | # result = await penetration_report.save() 461 | # 462 | # res_str = get_std_json_response(code=200, data=jsontool.dumps(result.to_dict())) 463 | # return res_str 464 | # 465 | # 466 | # class CreatePenetrationReportNote(MyUserBaseHandler): 467 | # # TODO: change to motor 468 | # """ 469 | # 渗透测试数据详情 470 | # """ 471 | # 472 | # @app_token_required() 473 | # @my_async_jsonp 474 | # async def post(self): 475 | # penetration_report = PenetrationTestDataNote() 476 | # penetration_report.penetration_id = self.get_argument('penetration_id', None) 477 | # penetration_report.ip = self.get_argument('ip', None) 478 | # penetration_report.details = self.get_argument('details', None) 479 | # # penetration_report.MongoEmptyPassword = self.get_argument('MongoEmptyPassword', None) 480 | # # penetration_report.RedisEmptyPassword = self.get_argument('RedisEmptyPassword', None) 481 | # # penetration_report.SSHRootEmptyPassword = self.get_argument('SSHRootEmptyPassword', None) 482 | # penetration_report.set_default_rc_tag() 483 | # penetration_report.organization = await self.get_org_by_app() 484 | # await penetration_report.save() 485 | # 486 | # return ConstData.msg_succeed 487 | # 488 | # 489 | # class CreateProxyipReport(MyUserBaseHandler): 490 | # # TODO: change to motor 491 | # """ 492 | # 代理测试数据 493 | # """ 494 | # 495 | # @app_token_required() 496 | # @my_async_jsonp 497 | # async def post(self): 498 | # proxyip_report = ProxyipTestData() 499 | # proxyip_report.remoteip = self.get_argument('remoteip', None) 500 | # proxyip_report.originalip = self.get_argument('originalip', None) 501 | # proxyip_report.proxyip = self.get_argument('proxyip', None) 502 | # proxyip_report.set_default_rc_tag() 503 | # proxyip_report.organization = await self.get_org_by_app() 504 | # await proxyip_report.save() 505 | # 506 | # return ConstData.msg_succeed 507 | # 508 | # 509 | # class ReadProxyipReport(MyUserBaseHandler): 510 | # """ 511 | # 读取代理测试数据 512 | # """ 513 | # 514 | # @token_required() 515 | # @my_async_jsonp 516 | # @my_async_paginator 517 | # async def get(self): 518 | # pro_id = self.get_argument('pro_id', None) 519 | # if pro_id is None: 520 | # return await get_org_data(self, collection='proxyip_test_data') 521 | # else: 522 | # return await get_org_data(self, collection='proxyip_test_data', pro_id=pro_id) 523 | # 524 | # 525 | # class ReadPenetrationReport(MyUserBaseHandler): 526 | # """ 527 | # 读取渗透测试数据 528 | # """ 529 | # 530 | # @token_required() 531 | # @my_async_jsonp 532 | # @my_async_paginator 533 | # async def get(self): 534 | # pro_id = self.get_argument('pro_id', None) 535 | # if pro_id is None: 536 | # return await get_org_data(self, collection='penetration_test_data') 537 | # else: 538 | # return await get_org_data(self, collection='penetration_test_data', pro_id=pro_id) 539 | # 540 | # 541 | # class ReadPenetrationReportNote(MyUserBaseHandler): 542 | # # TODO: change to motor 543 | # """ 544 | # 读取渗透测试数据详情 545 | # """ 546 | # 547 | # @token_required() 548 | # @my_async_jsonp 549 | # # @my_async_paginator 550 | # async def get(self): 551 | # penetration_id = self.get_argument('penetration_id', None) 552 | # res = await PenetrationTestDataNote.objects.filter(penetration_id=penetration_id).find_all() 553 | # result = [] 554 | # for item in res: 555 | # result.append(item.to_dict()) 556 | # data = get_std_json_response(code=200, data=jsontool.dumps(result)) 557 | # return data 558 | # 559 | # 560 | # class ReadDetailTestData(MyUserBaseHandler): 561 | # # TODO: change to motor 562 | # """ 563 | # 564 | # """ 565 | # 566 | # @token_required() 567 | # @my_async_jsonp 568 | # async def get(self): 569 | # id = self.get_argument('id', None) 570 | # callback = self.get_argument('callback', None) 571 | # res = await ApiTestDataNote.objects.filter(apitestdata_id=id).find_all() 572 | # result = [] 573 | # for item in res: 574 | # result.append(item.to_dict()) 575 | # data = get_std_json_response(code=200, data=jsontool.dumps(result)) 576 | # return data 577 | # 578 | # 579 | # class ReadTestDataTag(MyUserBaseHandler): 580 | # """ 581 | # --因为性能问题,取消使用 2016-09-30 582 | # """ 583 | # 584 | # @token_required() 585 | # @my_async_jsonp 586 | # async def get(self): 587 | # id = self.get_argument('id', None) 588 | # res = await ApiTestData.objects.get(id=ObjectId(id)) 589 | # return get_std_json_response(code=200, data=jsontool.dumps(res.to_dict())) 590 | # 591 | # 592 | # class CreateFeedback(MyUserBaseHandler): 593 | # # TODO: change to motor 594 | # """ 595 | # 上传文件 596 | # """ 597 | # 598 | # @token_required() 599 | # @my_async_jsonp 600 | # async def post(self): 601 | # upload_path = os.path.join(os.path.dirname(__file__), 'files') # 文件的暂存路径 602 | # file_metas = self.request.files['uploadfile'] # 提取表单中‘name’为‘file’的文件元数据 603 | # msg = self.get_argument('msg', None) 604 | # if list_have_none_mem(*[msg]): 605 | # return ConstData.msg_args_wrong 606 | # 607 | # file_list = [] 608 | # for meta in file_metas: 609 | # filename = meta['filename'] 610 | # # filepath = os.path.join(upload_path, filename) 611 | # filepath = '/var/static/' + filename 612 | # with open(filepath, 'wb') as up: # 有些文件需要已二进制的形式存储,实际中可以更改 613 | # up.write(meta['body']) 614 | # file_list.append(filename) 615 | # 616 | # feedback_msg = FeedbackMsg() 617 | # feedback_msg.msg = msg 618 | # feedback_msg.file_path = filepath 619 | # await feedback_msg.save() 620 | # return ConstData.msg_succeed 621 | # 622 | # 623 | # class FeedbackList(MyUserBaseHandler): 624 | # """ 625 | # 查看列表 626 | # """ 627 | # 628 | # @token_required() 629 | # @my_async_jsonp 630 | # @my_async_paginator_list 631 | # async def get(self): 632 | # mongo_coon = self.get_async_mongo() 633 | # 634 | # res = await mongo_coon['feedback_msg'].find({}) 635 | # list = [] 636 | # for i in res: 637 | # list.append(i.to_list()) 638 | # return list 639 | -------------------------------------------------------------------------------- /apps/testdata/urls.py: -------------------------------------------------------------------------------- 1 | from dtlib import filetool 2 | 3 | from apps.testdata.handlers import * 4 | 5 | app_path = filetool.get_parent_folder_name(__file__) # 'apis' # set the relative path in one place 6 | 7 | url = [ 8 | # 测试结果插入数据 9 | 10 | 11 | (r"/%s/api-auth/" % app_path, ApiAuth), 12 | (r"/%s/api-auth-out/" % app_path, ApiAuthout), 13 | 14 | # api_auth纯接口区jsonp,不涉及UI, 15 | # (r"/%s/create-perform-report/" % app_path, CreatePerformReport), # 服务端性能测试报告 16 | # (r"/%s/create-safety-test-report/" % app_path, CreateSafetyTestReport), # 安全数据 17 | # (r"/%s/create-api-req-delay/" % app_path, CreateApiReqDelay), # 模拟浏览器器请求的响应时间 18 | # (r"/%s/create-penetration-test-data/" % app_path, CreatePenetrationReport), # 插入渗透测试数据 19 | # (r"/%s/create-penetration-test-data-note/" % app_path, CreatePenetrationReportNote), # 插入渗透测试数据详情 20 | # (r"/%s/create-proxyip-test-data/" % app_path, CreateProxyipReport), # 插入代理ip测试数据 21 | 22 | (r"/%s/create-test-data/" % app_path, CreateUnitTestData), # 创建完整的测试数据--自动化高频接口 23 | (r"/%s/list-test-data/" % app_path, ListUnitTestData), # 显示分页信息,不使用ORM 24 | (r"/%s/get-one-test-data/" % app_path, GetOneUnitTestData), # 显示一个数据信息 25 | 26 | # login_auth分页信息显示-jsonp 27 | (r"/%s/read-server-api-testdata/" % app_path, ListApiTestData), # 接口测试 28 | (r"/%s/read-safety-testdata/" % app_path, ListSafetyTestData), # 安全测试 29 | (r"/%s/read-performance-testdata/" % app_path, ListPerformanceTestData), # 压力测试 30 | (r"/%s/read-api-req-delay-testedata/" % app_path, ListApiReqDelay), # 响应时间的显示 31 | # (r"/%s/read-penetration-test-data/" % app_path, ReadPenetrationReport), # 读取爆破测试数据 32 | # (r"/%s/read-penetration-test-data-note/" % app_path, ReadPenetrationReportNote), # 读取爆破测试数据 33 | # (r"/%s/read-proxyip-test-data/" % app_path, ReadProxyipReport), # 读取代理ip测试数据 34 | 35 | (r"/%s/delete-test-data/" % app_path, DeleteTestData), # 删除接口测试数据 36 | 37 | # (r"/%s/feedback/" % app_path, CreateFeedback), # 38 | # (r"/%s/feedback-list/" % app_path, FeedbackList), # 39 | 40 | ] 41 | -------------------------------------------------------------------------------- /apps/webapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/webapp/__init__.py -------------------------------------------------------------------------------- /apps/webapp/handlers.py: -------------------------------------------------------------------------------- 1 | from bson import ObjectId 2 | from dtlib import jsontool 3 | from dtlib.aio.decos import my_async_jsonp 4 | from dtlib.tornado.decos import token_required 5 | # from dtlib.tornado.docs import TestDataApp 6 | from dtlib.utils import list_have_none_mem 7 | from dtlib.web.constcls import ConstData 8 | from dtlib.web.tools import get_std_json_response 9 | 10 | from dtlib.tornado.base_hanlder import MyUserBaseHandler 11 | 12 | 13 | class GetOrgTestApps(MyUserBaseHandler): 14 | """ 15 | 获取某个组织的自动化APP 16 | """ 17 | 18 | @token_required() 19 | @my_async_jsonp 20 | async def get(self): 21 | org_id = self.get_argument('org_id', None) 22 | 23 | if list_have_none_mem(*[org_id, ]): 24 | return ConstData.msg_args_wrong 25 | 26 | # org = await Organization.objects.get(id=ObjectId(str(org_id))) 27 | # """:type Organization""" 28 | # if org is None: 29 | # return ConstData.msg_none 30 | 31 | db = self.get_async_mongo() 32 | app_col = db.test_data_app 33 | 34 | test_app = await app_col.find_one({ 35 | 'organization': ObjectId(str(org_id)) 36 | }) 37 | 38 | # test_app = await TestDataApp.objects.get(organization=ObjectId(str(org_id))) # 如果后面是1对多了,则查询多条 39 | 40 | if test_app is None: 41 | return ConstData.msg_none 42 | else: 43 | # result = [] 44 | # result.append(test_app.to_dict()) 45 | return get_std_json_response(code=200, data=jsontool.dumps(test_app, ensure_ascii=False)) 46 | 47 | # code trash (2018-04-23 yx) 48 | # class GetCurrentOrgTestApp(MyUserBaseHandler): 49 | # """ 50 | # 获取当前组织的自动化APP 51 | # """ 52 | # 53 | # @token_required() 54 | # @my_async_jsonp 55 | # async def get(self): 56 | # organization = await self.get_organization() 57 | # """:type Organization""" 58 | # app = await TestDataApp.objects.get(organization=ObjectId(organization.get_id())) 59 | # 60 | # if app is None: 61 | # return ConstData.msg_none 62 | # else: 63 | # result = [] 64 | # 65 | # result.append(app.to_dict()) 66 | # return get_std_json_response(code=200, data=jsontool.dumps(result)) 67 | -------------------------------------------------------------------------------- /apps/webapp/urls.py: -------------------------------------------------------------------------------- 1 | from dtlib import filetool 2 | from apps.webapp.handlers import * 3 | 4 | app_path = filetool.get_parent_folder_name(__file__) # set the relative path in one place 5 | 6 | url = [ 7 | 8 | 9 | # todo: 这个后面不再用了,因为后面应用是和组织绑定了,谁拥有组织,谁就有应用的读写权限 10 | # (r"/%s/get-auth-user-test-app/" % app_path, GetCurrentOrgTestApp), # 获取当前用户的应用ID/KEY 11 | # 12 | (r"/%s/get-org-test-apps/" % app_path, GetOrgTestApps), # 获取当前用户的应用ID/KEY 13 | ] 14 | -------------------------------------------------------------------------------- /apps/ws/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/apps/ws/__init__.py -------------------------------------------------------------------------------- /apps/ws/handlers.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | 3 | """ 4 | 建立长连接的websocket 5 | """ 6 | from tornado import websocket 7 | from tornado.gen import coroutine 8 | from dtlib.tornado.tools import call_subprocess 9 | from config import AUTOTEST_CMD 10 | 11 | 12 | class ApiTestsSocket(websocket.WebSocketHandler): 13 | """ 14 | 执行API脚本的WS请求 15 | """ 16 | 17 | ws_client = None # 请求的客户端 18 | 19 | def check_origin(self, origin): 20 | return True 21 | 22 | def open(self): 23 | """ 24 | 连接websocket服务器时进行的event 25 | """ 26 | 27 | ApiTestsSocket.ws_client = self # 添加websocket 28 | 29 | print("WebSocket opened") 30 | 31 | @coroutine 32 | def on_message(self, message): 33 | """ 34 | 收到信息的时候进行的动作 35 | """ 36 | # write_message用于主动写信息,这里将收到的信息原样返回 37 | # yield sleep(5) 38 | print('Websocket receive message') 39 | # self.write_message(ConstData.msg_fail) 40 | if message == "exec_api_tests": 41 | cmd = AUTOTEST_CMD # 执行自动化脚本任务 42 | # cmd = 'sleep 5' 43 | result, error = yield call_subprocess(cmd) 44 | else: 45 | pass 46 | # self.write_message(ConstData.msg_fail) 47 | # self.write_message(u"You said: " + message) 48 | 49 | def on_close(self): 50 | """ 51 | 关闭连接时的动作 52 | """ 53 | ApiTestsSocket.ws_client = None 54 | -------------------------------------------------------------------------------- /apps/ws/urls.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | """ 3 | websocket相关接口 4 | """ 5 | 6 | 7 | from apps.ws.handlers import ApiTestsSocket 8 | from dtlib import filetool 9 | 10 | app_path = filetool.get_parent_folder_name(__file__) 11 | 12 | url = [ 13 | (r"/%s/exec-api-tests/" % app_path, ApiTestsSocket), # 执行服务 14 | ] 15 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | 5 | from dtlib.dbtool import DbSetting 6 | 7 | app_version = '4.18.5.21.1' # 项目版本号 8 | test_token = '1234' # 用于超级管理员调用url的简单的token 9 | 10 | # if you use tornado project,this returns the root of the project 11 | BASE_DIR = os.path.abspath(os.path.curdir) 12 | debug_mode = False # 是否是调试模式 13 | 14 | settings = { 15 | "cookie_secret": "42233434", 16 | # 'xsrf_cookies':True, 17 | "debug": False, # 非调试模式 18 | "static_path": os.path.join(BASE_DIR, "static"), # 静态文件目录 19 | # "login_url":"/", 20 | "template_path": os.path.join(BASE_DIR, "static/templates"), # 模板目录 21 | } 22 | 23 | HTTP_PORT = 8011 # 启动本app的端口 24 | SERVER_PROCESS = 0 # 设置进程数,0表示利用所有的CPU核心 25 | 26 | # 根据环境变量,切换开发态和生产态 27 | try: 28 | docker_flag = os.environ.get('DOCKER', "") 29 | # TODO: add online config. 30 | if docker_flag == '1': 31 | mongo_host = 'mongo' 32 | print('Run in docker!') 33 | else: 34 | mongo_host = '127.0.0.1' 35 | except: 36 | print("Unexpected error:", sys.exc_info()[0]) 37 | raise 38 | 39 | # ======Mongodb=== 40 | mongodb_cfg = DbSetting( 41 | alias='default', 42 | host=mongo_host, 43 | port=27017, 44 | db_name='xtest', 45 | user_name='xtest', 46 | user_pwd='xtest@2018', 47 | max_connections=10 48 | ) 49 | 50 | LOGGING_LEVEL = logging.ERROR # 日志输出的级别 51 | LOGGING_STREAM = '/dev/stdout' # 重定向到控制台 52 | 53 | AUTOTEST_CMD = 'python /api-test-template/run.py' # 自动化测试脚本 54 | 55 | if __name__ == '__main__': 56 | # BASE_DIR = os.path.abspath('.') 57 | # print BASE_DIR 58 | pass 59 | -------------------------------------------------------------------------------- /config_api.py: -------------------------------------------------------------------------------- 1 | """ 2 | 本服务特有的config,和标准的模板config独立出来 3 | """ 4 | BASE_DOMAIN = 'http://api.apiapp.cc' # 根域名,一些全局变量里面会用到 5 | QR_AUTH_DOMAIN = 'qrauth://api.apiapp.cc' # 二维码用于扫码认证的 6 | AUTH_CALLBACK_DOMAIN = 'http://xtest.apiapp.cc/' 7 | capt_image_domain = 'http://192.168.1.200:8079' # 本地打码图片服务器 8 | -------------------------------------------------------------------------------- /init_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | 初始化app的运行环境 3 | - 多核心环境设计有竞争的资源时,会有线程冲突 4 | """ 5 | import asyncio 6 | import motor 7 | 8 | from config import mongodb_cfg 9 | from dtlib.mongo.utils import set_mongo_ttl_index 10 | 11 | 12 | async def init_mongo(): 13 | """ 14 | 不能在多核心状态进行设置 15 | :return: 16 | """ 17 | mongo_conn = motor.MotorClient( 18 | host=mongodb_cfg.host, 19 | port=mongodb_cfg.port, 20 | ) 21 | db = mongo_conn[mongodb_cfg.db_name] 22 | 23 | res = await db.authenticate( 24 | mongodb_cfg.user_name, 25 | mongodb_cfg.user_pwd, 26 | ) 27 | if res is True: 28 | log_session = mongo_conn.log_session 29 | index_filed_name = 'last_use_time' 30 | ttl_seconds = 60 * 60 * 48 # 2days 31 | await set_mongo_ttl_index(log_session, index_filed_name, ttl_seconds) 32 | print('set session ttl succeed...') 33 | 34 | 35 | if __name__ == '__main__': 36 | init_loop = asyncio.get_event_loop() 37 | init_loop.run_until_complete(init_mongo()) 38 | # init_loop.stop() 39 | print('init app succeed') 40 | -------------------------------------------------------------------------------- /myapplication.py: -------------------------------------------------------------------------------- 1 | from tornado.web import Application 2 | 3 | from dtlib.tornado.const_data import FieldDict 4 | from dtlib.web.valuedict import ValueDict 5 | 6 | commentKeep_ValueDict = ValueDict(0, '') 7 | 8 | 9 | class MyApplication(Application): 10 | """ 11 | 加入一些自定义的应用 12 | """ 13 | 14 | def set_async_redis(self, async_rc_pool): 15 | """ 16 | 设置连接池 17 | :param async_rc_pool: 18 | :return: 19 | """ 20 | self.settings[FieldDict.key_async_redis_pool] = async_rc_pool 21 | pass 22 | 23 | def set_sync_redis(self, sync_rc_pool): 24 | """ 25 | 获取同步类型的redis的连接池 26 | :return: 27 | """ 28 | self.settings[FieldDict.key_sync_redis_pool] = sync_rc_pool 29 | pass 30 | 31 | def set_async_mongo(self, async_mongo_pool): 32 | """ 33 | 异步的mongo连接池 34 | :param async_mongo_pool: 35 | :return: 36 | """ 37 | self.settings[FieldDict.key_async_mongo_pool] = async_mongo_pool 38 | -------------------------------------------------------------------------------- /nginx_config/test-api.conf: -------------------------------------------------------------------------------- 1 | #demo of nginx config 2 | # the upstream component nginx needs to connect to 3 | 4 | upstream gtt-webapi{ 5 | server 127.0.0.1:8011; 6 | } 7 | 8 | 9 | # configuration of the server 10 | server { 11 | # the port your site will be served on 12 | listen 8009; 13 | # the domain name it will serve for 14 | server_name 127.0.0.1; # substitute your machine's IP address or FQDN 15 | charset utf-8; 16 | # max upload size 17 | client_max_body_size 75M; # adjust to taste 18 | 19 | sendfile on; 20 | keepalive_timeout 0; 21 | 22 | 23 | location / { 24 | 25 | if ($request_method = 'OPTIONS') { 26 | add_header 'Access-Control-Allow-Origin' '*'; 27 | add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; 28 | # 29 | # Custom headers and headers various browsers *should* be OK with but aren't 30 | # 31 | add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; 32 | # 33 | # Tell client that this pre-flight info is valid for 20 days 34 | # 35 | add_header 'Access-Control-Max-Age' 1728000; 36 | add_header 'Content-Type' 'text/plain charset=UTF-8'; 37 | add_header 'Content-Length' 0; 38 | return 204; 39 | } 40 | 41 | proxy_pass_header Server; 42 | proxy_set_header Host $http_host; 43 | proxy_redirect off; 44 | proxy_set_header X-Real-IP $remote_addr; 45 | proxy_set_header X-Scheme $scheme; 46 | proxy_pass http://gtt-webapi; 47 | add_header Access-Control-Allow-Origin *; 48 | add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; 49 | #add_header Access-Control-Allow-Credentials true; 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /nginx_config/test.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8099; 3 | server_name 192.168.1.200; 4 | root /www/xtest-web/dist/; 5 | 6 | location ~ .*\.(jpg|jpeg|png|webp)$ { 7 | add_header "Access-Control-Allow-Origin" "*"; 8 | add_header "Access-Control-Allow-Headers" "X-Requested-With"; 9 | add_header "Access-Control-Allow-Methods" "GET,POST,OPTIONS"; 10 | expires 7d; 11 | } 12 | location ~ .*\.(js|css)?$ 13 | { 14 | expires 1h; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # python package requirements 2 | 3 | requests==2.18.4 4 | 5 | #mongodb 6 | motor==1.2.1 7 | 8 | tornado==4.3 9 | #torndsession==1.1.4 10 | 11 | tabulate==0.8.2 12 | 13 | # xtest base 14 | https://github.com/ityoung/dtlib/archive/master.zip 15 | -------------------------------------------------------------------------------- /route.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tornado.web import StaticFileHandler 4 | 5 | # from config import * 6 | from config import settings, BASE_DIR 7 | from dtlib.tornado.tools import get_apps_url 8 | 9 | urls = get_apps_url(BASE_DIR) 10 | 11 | try: 12 | from xt_wechat.urls import url 13 | # TODO: set online flag. 14 | urls += url 15 | except: 16 | pass 17 | 18 | urls += [ 19 | (r"/favicon.ico", StaticFileHandler, {"path": "/static/favicon.ico"}), 20 | (r"/js/(.*)", StaticFileHandler, {"path": os.path.join(settings['static_path'], 'js')}), 21 | (r"/css/(.*)", StaticFileHandler, {"path": os.path.join(settings['static_path'], 'css')}), 22 | (r"/img/(.*)", StaticFileHandler, {"path": os.path.join(settings['static_path'], 'img')}), 23 | (r"/content/(.*)", StaticFileHandler, {"path": os.path.join(settings['static_path'], 'content')}), 24 | 25 | ] 26 | 27 | # todo 后续写成 include的方式,更直观 28 | -------------------------------------------------------------------------------- /service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exe_project_name="xt-server-api" 3 | python_file_name="start.py" 4 | 5 | #shell code 6 | exe_file_folder="/www/$exe_project_name" 7 | exe_file_tag="$exe_project_name/$python_file_name" 8 | exe_file_path="$exe_file_folder/$python_file_name" 9 | log_path="/var/log/$exe_project_name/$exe_project_name.log" 10 | 11 | case "$1" in 12 | start) 13 | echo "start $exe_project_name" 14 | cd $exe_file_folder 15 | nohup python $exe_file_path 1>$log_path 2>$log_path.error & 16 | echo $exe_file_path 17 | echo $log_path 18 | echo $log_path.error 19 | ;; 20 | restart) 21 | echo "kill $exe_project_name" 22 | ps -ef|grep python |grep $exe_file_tag|awk '{print $2}'|xargs kill -9 23 | 24 | echo "start $exe_project_name" 25 | cd $exe_file_folder 26 | nohup python $exe_file_path 1>$log_path 2>$log_path.error & 27 | echo $exe_file_path 28 | echo $log_path 29 | echo $log_path.error 30 | ;; 31 | stop) 32 | echo "kill $exe_project_name" 33 | ps -ef|grep python |grep $exe_file_tag|awk '{print $2}'|xargs kill -9 34 | ;; 35 | esac -------------------------------------------------------------------------------- /start.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | """ 3 | 4 | - 可以使用tornado server 5 | - 修改ioloop绑定后,可多核启动 6 | 7 | """ 8 | 9 | import asyncio 10 | import logging 11 | 12 | import motor.motor_asyncio 13 | from tabulate import tabulate 14 | 15 | import config 16 | from config import HTTP_PORT, settings, mongodb_cfg, LOGGING_LEVEL, debug_mode, SERVER_PROCESS 17 | from dtlib.dtlog import dlog 18 | from dtlib.tornado.async_server import MyAsyncHttpServer 19 | from route import urls 20 | from myapplication import MyApplication 21 | 22 | 23 | def output_server_info(): 24 | """ 25 | 输出服务器相关配置信息 26 | :param kwargs: 27 | :return: 28 | """ 29 | 30 | tbl_out = tabulate([ 31 | ['app_version', config.app_version], 32 | ['http_port', config.HTTP_PORT], 33 | ['process_num ', config.SERVER_PROCESS], 34 | ['db_server', config.mongodb_cfg.host] 35 | ], 36 | tablefmt='grid') 37 | print(tbl_out) 38 | print('http://127.0.0.1:%s' % config.HTTP_PORT) 39 | 40 | 41 | async def auth_mongodb(**kwargs): 42 | """ 43 | 异步身份认证mongodb 44 | :type future:asyncio.Future 45 | :param kwargs: 46 | :return: 47 | """ 48 | mongo_conn = kwargs.get('mongo_conn', None) 49 | 50 | if mongo_conn is None: 51 | raise ValueError 52 | 53 | res = await mongo_conn.authenticate( 54 | mongodb_cfg.user_name, 55 | mongodb_cfg.user_pwd, 56 | ) 57 | if res is True: 58 | print('mongodb authenticate succeed...') 59 | 60 | async def auth_mongo(**kwargs): 61 | 62 | db = kwargs.get('db', None) 63 | 64 | if mongo_conn is None: 65 | raise ValueError 66 | res = await db.authenticate( 67 | mongodb_cfg.user_name, 68 | mongodb_cfg.user_pwd, 69 | ) 70 | 71 | if res is True: 72 | print('mongodb authenticate succeed...') 73 | 74 | if __name__ == '__main__': 75 | # 设置日志输出级别 76 | dlog.setLevel(LOGGING_LEVEL) 77 | logging.getLogger('asyncio').setLevel(logging.WARNING) 78 | 79 | # 输出运行服务的相关信息 80 | output_server_info() 81 | 82 | app = MyApplication( 83 | urls, 84 | **settings 85 | ) 86 | 87 | server = MyAsyncHttpServer( 88 | app, 89 | xheaders=True # when running behind a load balancer like nginx 90 | ) 91 | server.bind(HTTP_PORT) # 设置端口 92 | server.start(num_processes=SERVER_PROCESS) # if 0,forks one process per cpu,#TORNADO_SERVER_PROCESS 93 | 94 | # asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # 使用uvloop来进行loop替换 95 | io_loop = asyncio.get_event_loop() 96 | io_loop.set_debug(debug_mode) 97 | 98 | # 设置Mongodb 99 | # you only need to keep track of the DB instance if you connect to multiple databases. 100 | mongo_conn = motor.motor_asyncio.AsyncIOMotorClient(mongodb_cfg.host,mongodb_cfg.port) 101 | # mongo_conn.max_pool_size = mongodb_cfg.max_connections # mongodb连接池最大连接数 102 | db=mongo_conn.xtest 103 | io_loop.run_until_complete(auth_mongo(db=db)) # 数据库认证 104 | 105 | # 设置异步mongo连接,方便后续直接非ORM操作,比如请求大量的数据 106 | app.set_async_mongo(db) 107 | 108 | io_loop.run_forever() 109 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x-utest/xtest-server/4d7c05266dd42ebf4a1ddc416eb615410b40b5fb/static/favicon.ico -------------------------------------------------------------------------------- /static/templates/code/argument.codetpl: -------------------------------------------------------------------------------- 1 | 2 | #路由值 3 | (r"/%s/create-{{low_cls_name}}/" % app_path, Create{{cls_name}}), 4 | (r"/%s/read-{{low_cls_name}}/" % app_path, Read{{cls_name}}), 5 | (r"/%s/update-{{low_cls_name}}/" % app_path, Update{{cls_name}}), 6 | (r"/%s/delete-{{low_cls_name}}/" % app_path, Delete{{cls_name}}), 7 | (r"/%s/list-{{low_cls_name}}/" % app_path, List{{cls_name}}),#分页列表 8 | 9 | #region {{cls_name}} CRUBL 10 | 11 | class Create{{cls_name}}(BaseHandler): 12 | """ 13 | 创建{{cls_name}} 14 | """ 15 | def get(self): 16 | self.write('get') 17 | 18 | @cor_login_required 19 | @coroutine 20 | def post(self): 21 | #表单提取 22 | {%for x in line%} 23 | {{x}} = self.get_argument("{{x}}",None){%end%} 24 | 25 | #空值检查 26 | if list_have_none_mem(*[{%for x in line%}{{x}},{%end%}]) is True: 27 | raise Return(ConstData.msg_args_wrong) 28 | 29 | #数据表赋值 30 | doc_obj = {{cls_name}}() 31 | {%for x in line%} 32 | doc_obj.{{x}} = {{x}}{%end%} 33 | doc_obj.set_default_rc_tag() 34 | yield doc_obj.save() 35 | 36 | raise Return(ConstData.msg_succeed) 37 | 38 | 39 | class Read{{cls_name}}(BaseHandler): 40 | """ 41 | 创建{{cls_name}} 42 | """ 43 | def get(self): 44 | self.write('get') 45 | 46 | def post(self): 47 | self.write('post') 48 | 49 | 50 | class Update{{cls_name}}(BaseHandler): 51 | """ 52 | 更新{{cls_name}} 53 | """ 54 | def get(self): 55 | self.write('get') 56 | 57 | def post(self): 58 | self.write('post') 59 | 60 | 61 | class Delete{{cls_name}}(BaseHandler): 62 | """ 63 | 删除{{cls_name}} 64 | """ 65 | def get(self): 66 | self.write('get') 67 | 68 | def post(self): 69 | self.write('post') 70 | 71 | class List{{cls_name}}(BaseHandler): 72 | """ 73 | 分页列表{{cls_name}} 74 | """ 75 | def get(self): 76 | self.write('get') 77 | 78 | def post(self): 79 | self.write('post') 80 | 81 | 82 | #endregion 83 | 84 | 85 | #region 接口测试脚本 86 | 87 | class {{cls_name}}ApiTest(BaseTest): 88 | """ 89 | 本项目的重要的接口测试脚本,需要登录的 90 | """ 91 | 92 | def test_create_{{underline_cls_name}}(self): 93 | """ 94 | """ 95 | url = "%s/%s/create-{{low_cls_name}}/" % (self.base_url, app_path) 96 | 97 | params = dict({%for x in line%} 98 | {{x}} = 'xxx',{%end%} 99 | ) 100 | 101 | res = self.se_req.post(url, data=params) 102 | dlog.debug(res.text) 103 | res_dict = json.loads(res.text) 104 | self.assertEqual(res_dict['code'], 200) 105 | 106 | def test_read_{{underline_cls_name}}(self): 107 | """ 108 | """ 109 | url = "%s/%s/read-{{low_cls_name}}/" % (self.base_url, app_path) 110 | self.assertTrue(True) 111 | 112 | def test_update_{{underline_cls_name}}(self): 113 | """ 114 | """ 115 | url = "%s/%s/update-{{low_cls_name}}/" % (self.base_url, app_path) 116 | 117 | params = dict({%for x in line%} 118 | {{x}} = 'xxx',{%end%} 119 | ) 120 | 121 | res = self.se_req.post(url, data=params) 122 | dlog.debug(res.text) 123 | res_dict = json.loads(res.text) 124 | self.assertEqual(res_dict['code'], 200) 125 | 126 | 127 | def test_delete_{{underline_cls_name}}(self): 128 | """ 129 | """ 130 | url = "%s/%s/delete-{{low_cls_name}}/" % (self.base_url, app_path) 131 | 132 | params = dict({%for x in line%} 133 | {{x}} = 'xxx',{%end%} 134 | ) 135 | 136 | res = self.se_req.post(url, data=params) 137 | dlog.debug(res.text) 138 | res_dict = json.loads(res.text) 139 | self.assertEqual(res_dict['code'], 200) 140 | 141 | def test_list_{{underline_cls_name}}(self): 142 | """ 143 | """ 144 | url = "%s/%s/list-{{low_cls_name}}/" % (self.base_url, app_path) 145 | self.assertTrue(True) 146 | 147 | 148 | #endregion 149 | 150 | 151 | #前端分页代码 152 | 153 | 154 | {%for x in line%} 155 | {{x}}{%end%} 156 | 157 | 158 | 159 | {%for x in line%} 160 | {%end%} 161 | 162 | 163 | --------------------------------------------------------------------------------