├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ └── Project.xml ├── dbnavigator.xml ├── ginger.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── README.md ├── app ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ └── app.cpython-37.pyc ├── api │ ├── __init__.py │ ├── __pycache__ │ │ └── __init__.cpython-37.pyc │ └── v1 │ │ ├── __init__.py │ │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ ├── book.cpython-37.pyc │ │ ├── client.cpython-37.pyc │ │ ├── token.cpython-37.pyc │ │ └── user.cpython-37.pyc │ │ ├── book.py │ │ ├── client.py │ │ ├── token.py │ │ └── user.py ├── app.py ├── config │ ├── __pycache__ │ │ ├── secure.cpython-37.pyc │ │ └── setting.cpython-37.pyc │ └── setting.py ├── libs │ ├── __pycache__ │ │ ├── enums.cpython-37.pyc │ │ ├── error.cpython-37.pyc │ │ ├── error_code.cpython-37.pyc │ │ ├── redprint.cpython-37.pyc │ │ ├── scope.cpython-37.pyc │ │ └── token_auth.cpython-37.pyc │ ├── enums.py │ ├── error.py │ ├── error_code.py │ ├── redprint.py │ ├── scope.py │ └── token_auth.py ├── models │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-37.pyc │ │ ├── base.cpython-37.pyc │ │ └── user.cpython-37.pyc │ ├── base.py │ └── user.py └── validators │ ├── __init__.py │ ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── base.cpython-37.pyc │ └── forms.cpython-37.pyc │ ├── base.py │ └── forms.py ├── code.md ├── fake.py └── ginger.py /.gitignore: -------------------------------------------------------------------------------- 1 | secure.py 2 | 3 | uploads/ -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /workspace.xml -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 21 | 22 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.idea/dbnavigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | -------------------------------------------------------------------------------- /.idea/ginger.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ginger 2 | Flask Restful API权限管理系统(注册,登陆,认证,授权) 3 | 4 | 一套完整的Flask Restful风格的权限管理机制,以及Api的设计。 5 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from app.app import Flask 2 | 3 | 4 | def register_blueprints(app): 5 | from app.api.v1 import create_blueprint_v1 6 | app.register_blueprint(create_blueprint_v1(), url_prefix='/v1') 7 | 8 | 9 | def register_plugin(app): 10 | from app.models.base import db 11 | db.init_app(app) 12 | with app.app_context(): 13 | db.create_all() 14 | 15 | 16 | def create_app(): 17 | app = Flask(__name__) 18 | app.config.from_object('app.config.setting') 19 | app.config.from_object('app.config.secure') 20 | register_blueprints(app) 21 | register_plugin(app) 22 | return app 23 | -------------------------------------------------------------------------------- /app/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/__pycache__/app.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/__pycache__/app.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/__init__.py -------------------------------------------------------------------------------- /app/api/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from app.api.v1 import user, book, client, token 3 | 4 | 5 | def create_blueprint_v1(): 6 | bp_v1 = Blueprint('v1', __name__) 7 | user.api.register(bp_v1) 8 | book.api.register(bp_v1) 9 | client.api.register(bp_v1) 10 | token.api.register(bp_v1) 11 | return bp_v1 12 | -------------------------------------------------------------------------------- /app/api/v1/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/v1/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/__pycache__/book.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/v1/__pycache__/book.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/__pycache__/client.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/v1/__pycache__/client.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/__pycache__/token.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/v1/__pycache__/token.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/__pycache__/user.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/api/v1/__pycache__/user.cpython-37.pyc -------------------------------------------------------------------------------- /app/api/v1/book.py: -------------------------------------------------------------------------------- 1 | from app.libs.redprint import Redprint 2 | 3 | api = Redprint('book') 4 | 5 | 6 | @api.route('', methods=['GET']) 7 | def get_book(): 8 | a = 1 9 | return 'get book' 10 | 11 | 12 | @api.route('', methods=['POST']) 13 | def create_book(): 14 | return 'create book' 15 | -------------------------------------------------------------------------------- /app/api/v1/client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 5:40 下午 3 | # @Author : luYuZe 4 | # @File : client.py 5 | # @Project : ginger 6 | from flask import request 7 | 8 | from app.libs.enums import ClientTypeEnum 9 | from app.libs.error_code import Success 10 | from app.libs.redprint import Redprint 11 | from app.models.user import User 12 | from app.validators.forms import ClientForm, UserEmailForm 13 | 14 | api = Redprint('client') 15 | 16 | 17 | @api.route('/register', methods=['POST']) 18 | def create_client(): 19 | form = ClientForm().validate_for_api() 20 | promise = { 21 | ClientTypeEnum.USER_EMAIL: _register_user_by_email, 22 | } 23 | promise[form.type.data]() 24 | return Success() 25 | 26 | 27 | def _register_user_by_email(): 28 | form = UserEmailForm().validate_for_api() 29 | User.register_by_email(form.nickname.data, form.account.data, form.secret.data) 30 | -------------------------------------------------------------------------------- /app/api/v1/token.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/5 7:55 下午 3 | # @Author : luYuZe 4 | # @File : token.py 5 | # @Project : ginger 6 | from flask import current_app, jsonify 7 | 8 | from app.libs.enums import ClientTypeEnum 9 | from app.libs.redprint import Redprint 10 | from app.models.user import User 11 | from app.validators.forms import ClientForm 12 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer 13 | 14 | api = Redprint('token') 15 | 16 | 17 | @api.route('', methods=['POST']) 18 | def get_token(): 19 | form = ClientForm().validate_for_api() 20 | promise = { 21 | ClientTypeEnum.USER_EMAIL: User.verify, 22 | } 23 | identity = promise[ClientTypeEnum(form.type.data)](form.account.data, form.secret.data) 24 | expiration = current_app.config['TOKEN_EXPIRATION'] 25 | token = generate_auth_token(identity['uid'], form.type.data, identity['scope'], expiration) 26 | t = { 27 | "token": token.decode('ascii') 28 | } 29 | return jsonify(t), 201 30 | 31 | 32 | def generate_auth_token(uid, ac_type, scope=None, expiration=7200): 33 | s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration) 34 | return s.dumps({ 35 | 'uid': uid, 36 | 'type': ac_type.value, 37 | 'scope': scope 38 | }) 39 | -------------------------------------------------------------------------------- /app/api/v1/user.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Blueprint, g, request 4 | from flask.json import jsonify 5 | 6 | from app.libs.error_code import DeleteSuccess, AuthFailed, Success 7 | from app.libs.redprint import Redprint 8 | from app.libs.token_auth import auth 9 | from app.models.base import db 10 | from app.models.user import User 11 | 12 | api = Redprint('user') 13 | 14 | 15 | # 查询指定用户,超级权限 16 | @api.route('/', methods=['GET']) 17 | @auth.login_required 18 | def super_get_user(uid): 19 | user = User.query.filter_by(id=uid).first_or_404() 20 | return jsonify(user) 21 | 22 | # 查询本用户 23 | @api.route('', methods=['GET']) 24 | @auth.login_required 25 | def get_user(): 26 | uid = g.user.uid 27 | user = User.query.filter_by(id=uid).first_or_404() 28 | return jsonify(user) 29 | 30 | # 删除指定用户 31 | @api.route('/', methods=['DELETE']) 32 | def super_delete_user(uid): 33 | pass 34 | 35 | # 删除本用户 36 | @api.route('', methods=['DELETE']) 37 | @auth.login_required 38 | def delete_user(): 39 | uid = g.user.uid 40 | with db.auto_commit(): 41 | user = User.query.filter_by(id=uid).first_or_404() 42 | user.delete() 43 | return DeleteSuccess() 44 | 45 | 46 | @api.route('', methods=['POST']) 47 | def create_user(): 48 | return 'hello' 49 | 50 | 51 | @api.route('/avatar', methods=['POST']) 52 | def avatar(): 53 | file = request.files['avatar_upload'] 54 | base_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) 55 | suffix = file.filename.rsplit('.', 1)[1][:-1] 56 | filename = '10002' + '.' + suffix 57 | file_path = os.path.join(base_path, 'static', 'images', 'uploads', filename) 58 | file.save(file_path) 59 | return Success() 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/app.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from flask import Flask as _Flask 4 | from flask.json import JSONEncoder as _JSONEncoder 5 | 6 | from app.libs.error_code import ServerError 7 | 8 | 9 | class JSONEncoder(_JSONEncoder): 10 | def default(self, o): 11 | if hasattr(o, 'keys') and hasattr(o, '__getitem__'): 12 | return dict(o) 13 | if isinstance(o, date): 14 | return o.strftime('%Y-%m-%d') 15 | raise ServerError() 16 | 17 | 18 | class Flask(_Flask): 19 | json_encoder = JSONEncoder 20 | -------------------------------------------------------------------------------- /app/config/__pycache__/secure.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/config/__pycache__/secure.cpython-37.pyc -------------------------------------------------------------------------------- /app/config/__pycache__/setting.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/config/__pycache__/setting.cpython-37.pyc -------------------------------------------------------------------------------- /app/config/setting.py: -------------------------------------------------------------------------------- 1 | TOKEN_EXPIRATION = 30 * 24 * 3600 2 | ALBUMY_UPLOAD_PATH = '/static/images/' 3 | -------------------------------------------------------------------------------- /app/libs/__pycache__/enums.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/enums.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/__pycache__/error.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/error.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/__pycache__/error_code.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/error_code.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/__pycache__/redprint.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/redprint.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/__pycache__/scope.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/scope.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/__pycache__/token_auth.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/libs/__pycache__/token_auth.cpython-37.pyc -------------------------------------------------------------------------------- /app/libs/enums.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 5:44 下午 3 | # @Author : luYuZe 4 | # @File : enums.py 5 | # @Project : ginger 6 | from enum import Enum 7 | 8 | 9 | class ClientTypeEnum(Enum): 10 | USER_EMAIL = 100 11 | USER_MOBILE = 101 12 | # 微信小程序 13 | USER_MINA = 200 14 | # 微信公众号 15 | USER_WX = 201 16 | -------------------------------------------------------------------------------- /app/libs/error.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 10:48 下午 3 | # @Author : luYuZe 4 | # @File : error.py 5 | # @Project : ginger 6 | from flask import request, json 7 | from werkzeug.exceptions import HTTPException 8 | 9 | 10 | class APIException(HTTPException): 11 | code = 500 12 | msg = 'sorry, we made a mistake!' 13 | error_code = 999 14 | 15 | def __init__(self, msg=None, code=None, error_code=None, headers=None): 16 | if code: 17 | self.code = code 18 | if error_code: 19 | self.error_code = error_code 20 | if msg: 21 | self.msg = msg 22 | super(APIException, self).__init__(msg, None) 23 | 24 | def get_body(self, environ=None): 25 | body = dict( 26 | msg=self.msg, 27 | error_code=self.error_code, 28 | request=request.method + ' ' + self.get_url_no_param() 29 | ) 30 | text = json.dumps(body) 31 | return text 32 | 33 | def get_headers(self, environ=None): 34 | """Get a list of headers.""" 35 | return [('Content-Type', 'application/json')] 36 | 37 | @staticmethod 38 | def get_url_no_param(): 39 | full_path = str(request.full_path) 40 | main_path = full_path.split('?') 41 | return main_path[0] 42 | -------------------------------------------------------------------------------- /app/libs/error_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 10:38 下午 3 | # @Author : luYuZe 4 | # @File : error_code.py 5 | # @Project : ginger 6 | from app.libs.error import APIException 7 | 8 | 9 | class Success(APIException): 10 | code = 201 11 | msg = 'ok' 12 | error_code = 0 13 | 14 | 15 | class DeleteSuccess(Success): 16 | code = 202 17 | error_code = 1 18 | 19 | 20 | class ServerError(APIException): 21 | code = 500 22 | msg = 'sorry, we made a mistake!' 23 | error_code = 999 24 | 25 | 26 | class ClientTypeError(APIException): 27 | code = 400 28 | msg = 'client is invalid' 29 | error_code = 1006 30 | 31 | 32 | class ParameterException(APIException): 33 | code = 400 34 | msg = 'invalid parameter' 35 | error_code = 1000 36 | 37 | 38 | class NotFound(APIException): 39 | code = 404 40 | msg = 'the resource are not found' 41 | error_code = 1001 42 | 43 | 44 | class AuthFailed(APIException): 45 | code = 401 46 | msg = 'authorization failed' 47 | error_code = 1005 48 | 49 | 50 | class Forbidden(APIException): 51 | code = 403 52 | error_code = 1004 53 | msg = 'forbidden, not in scope' 54 | -------------------------------------------------------------------------------- /app/libs/redprint.py: -------------------------------------------------------------------------------- 1 | class Redprint(object): 2 | def __init__(self, name): 3 | self.name = name 4 | self.mound = [] 5 | 6 | def route(self, rule, **options): 7 | def decorator(f): 8 | self.mound.append((f, rule, options)) 9 | return f 10 | 11 | return decorator 12 | 13 | def register(self, bp, url_prefix=None): 14 | if url_prefix is None: 15 | url_prefix = "/" + self.name 16 | for f, rule, options in self.mound: 17 | endpoint = options.pop("endpoint", f.__name__) 18 | bp.add_url_rule(url_prefix + rule, endpoint, f, **options) 19 | -------------------------------------------------------------------------------- /app/libs/scope.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/7 10:31 下午 3 | # @Author : luYuZe 4 | # @File : scope.py 5 | # @Project : ginger 6 | 7 | 8 | class Scope: 9 | allow_api = [] 10 | 11 | def __add__(self, other): 12 | self.allow_api = self.allow_api + other.allow_api 13 | self.allow_api = list(set(self.allow_api)) 14 | return self 15 | 16 | 17 | class AdminScope(Scope): 18 | allow_api = ['v1.super_get_user'] 19 | 20 | def __init__(self): 21 | self + UserScope() 22 | 23 | 24 | class UserScope(Scope): 25 | allow_api = ['v1.A', 'v1.B'] 26 | 27 | 28 | class SuperScope(Scope): 29 | allow_api = ['v1.C', 'v1.D'] 30 | 31 | def __init__(self): 32 | self + UserScope() + AdminScope() 33 | 34 | 35 | def is_in_scope(scope, endpoint): 36 | scope = globals()[scope]() 37 | if endpoint in scope.allow_api: 38 | return True 39 | else: 40 | return False 41 | -------------------------------------------------------------------------------- /app/libs/token_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/5 8:50 下午 3 | # @Author : luYuZe 4 | # @File : token_auth.py 5 | # @Project : ginger 6 | from collections import namedtuple 7 | 8 | from flask import current_app, g, request 9 | from flask_httpauth import HTTPBasicAuth 10 | from itsdangerous import TimedJSONWebSignatureSerializer as Serializer, BadSignature, SignatureExpired 11 | 12 | from app.libs.error_code import AuthFailed, Forbidden 13 | from app.libs.scope import is_in_scope 14 | 15 | auth = HTTPBasicAuth() 16 | User = namedtuple('User', ['uid', 'ac_type', 'scope']) 17 | 18 | 19 | @auth.verify_password 20 | def verify_password(token, password): 21 | user_info = verify_auth_token(token) 22 | if not user_info: 23 | return False 24 | else: 25 | g.user = user_info 26 | return True 27 | 28 | 29 | def verify_auth_token(token): 30 | s = Serializer(current_app.config['SECRET_KEY']) 31 | try: 32 | data = s.loads(token) 33 | except BadSignature: 34 | raise AuthFailed(msg='token is invalid', error_code=1002) 35 | except SignatureExpired: 36 | raise AuthFailed(msg='token is expired', error_code=1003) 37 | uid = data['uid'] 38 | ac_type = data['type'] 39 | scope = data['scope'] 40 | allow = is_in_scope(scope, request.endpoint) 41 | if not allow: 42 | raise Forbidden() 43 | return User(uid, ac_type, scope) 44 | 45 | -------------------------------------------------------------------------------- /app/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 8:32 下午 3 | # @Author : luYuZe 4 | # @File : __init__.py.py 5 | # @Project : ginger 6 | 7 | -------------------------------------------------------------------------------- /app/models/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/models/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/models/__pycache__/base.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/models/__pycache__/base.cpython-37.pyc -------------------------------------------------------------------------------- /app/models/__pycache__/user.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/models/__pycache__/user.cpython-37.pyc -------------------------------------------------------------------------------- /app/models/base.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery 4 | from sqlalchemy import inspect, Column, Integer, SmallInteger, orm 5 | from contextlib import contextmanager 6 | 7 | from app.libs.error_code import NotFound 8 | 9 | 10 | class SQLAlchemy(_SQLAlchemy): 11 | @contextmanager 12 | def auto_commit(self): 13 | try: 14 | yield 15 | self.session.commit() 16 | except Exception as e: 17 | db.session.rollback() 18 | raise e 19 | 20 | 21 | class Query(BaseQuery): 22 | def filter_by(self, **kwargs): 23 | if 'status' not in kwargs.keys(): 24 | kwargs['status'] = 1 25 | return super(Query, self).filter_by(**kwargs) 26 | 27 | def get_or_404(self, ident): 28 | rv = self.get(ident) 29 | if not rv: 30 | raise NotFound() 31 | return rv 32 | 33 | def first_or_404(self): 34 | rv = self.first() 35 | if not rv: 36 | raise NotFound() 37 | return rv 38 | 39 | 40 | db = SQLAlchemy(query_class=Query) 41 | 42 | 43 | class Base(db.Model): 44 | __abstract__ = True 45 | create_time = Column(Integer) 46 | status = Column(SmallInteger, default=1) 47 | 48 | def __init__(self): 49 | self.create_time = int(datetime.now().timestamp()) 50 | 51 | def __getitem__(self, item): 52 | return getattr(self, item) 53 | 54 | @property 55 | def create_datetime(self): 56 | if self.create_time: 57 | return datetime.fromtimestamp(self.create_time) 58 | else: 59 | return None 60 | 61 | def set_attrs(self, attrs_dict): 62 | for key, value in attrs_dict.items(): 63 | if hasattr(self, key) and key != 'id': 64 | setattr(self, key, value) 65 | 66 | def delete(self): 67 | self.status = 0 68 | 69 | def keys(self): 70 | return self.fields 71 | 72 | def hide(self, *keys): 73 | for key in keys: 74 | self.fields.remove(key) 75 | return self 76 | 77 | def append(self, *keys): 78 | for key in keys: 79 | self.fields.append(key) 80 | return self 81 | 82 | 83 | class MixinJSONSerializer: 84 | @orm.reconstructor 85 | def init_on_load(self): 86 | self._fields = [] 87 | # self._include = [] 88 | self._exclude = [] 89 | 90 | self._set_fields() 91 | self.__prune_fields() 92 | 93 | def _set_fields(self): 94 | pass 95 | 96 | def __prune_fields(self): 97 | columns = inspect(self.__class__).columns 98 | if not self._fields: 99 | all_columns = set(columns.keys()) 100 | self._fields = list(all_columns - set(self._exclude)) 101 | 102 | def hide(self, *args): 103 | for key in args: 104 | self._fields.remove(key) 105 | return self 106 | 107 | def keys(self): 108 | return self._fields 109 | 110 | def __getitem__(self, key): 111 | return getattr(self, key) 112 | -------------------------------------------------------------------------------- /app/models/user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 8:33 下午 3 | # @Author : luYuZe 4 | # @File : user.py 5 | # @Project : ginger 6 | from sqlalchemy import Column, Integer, String, SmallInteger, orm 7 | from werkzeug.security import generate_password_hash, check_password_hash 8 | 9 | from app.libs.error_code import AuthFailed 10 | from app.models.base import Base, db 11 | 12 | 13 | class User(Base): 14 | id = Column(Integer, primary_key=True) 15 | email = Column(String(24), unique=True, nullable=False) 16 | nickname = Column(String(24), unique=True) 17 | auth = Column(SmallInteger, default=1) 18 | _password = Column('password', String(100)) 19 | 20 | @orm.reconstructor 21 | def __init__(self): 22 | self.fields = ['id', 'email', 'nickname', 'auth'] 23 | 24 | @property 25 | def password(self): 26 | return self._password 27 | 28 | @password.setter 29 | def password(self, raw): 30 | self._password = generate_password_hash(raw) 31 | 32 | @staticmethod 33 | def register_by_email(nickname, account, secret): 34 | with db.auto_commit(): 35 | user = User() 36 | user.nickname = nickname 37 | user.email = account 38 | user.password = secret 39 | db.session.add(user) 40 | 41 | @staticmethod 42 | def verify(email, password): 43 | user = User.query.filter_by(email=email).first_or_404() 44 | if not user.check_password(password): 45 | raise AuthFailed() 46 | scope = 'AdminScope' if user.auth == 2 else 'UserScope' 47 | return {'uid': user.id, "scope": scope} 48 | 49 | def check_password(self, raw): 50 | if not self._password: 51 | return False 52 | return check_password_hash(self._password, raw) 53 | -------------------------------------------------------------------------------- /app/validators/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 5:49 下午 3 | # @Author : luYuZe 4 | # @File : __init__.py.py 5 | # @Project : ginger 6 | 7 | -------------------------------------------------------------------------------- /app/validators/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/validators/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /app/validators/__pycache__/base.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/validators/__pycache__/base.cpython-37.pyc -------------------------------------------------------------------------------- /app/validators/__pycache__/forms.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luyuze95/ginger/61ab5e5efb3fc149ec94a1752167d61a24ead374/app/validators/__pycache__/forms.cpython-37.pyc -------------------------------------------------------------------------------- /app/validators/base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/4 10:47 下午 3 | # @Author : luYuZe 4 | # @File : base.py 5 | # @Project : ginger 6 | from flask import request 7 | from wtforms import Form 8 | 9 | from app.libs.error_code import ParameterException 10 | 11 | 12 | class BaseForm(Form): 13 | def __init__(self): 14 | data = request.get_json(slient=True) 15 | args = request.args.to_dict() 16 | super(BaseForm, self).__init__(data=data, **args) 17 | 18 | def validate_for_api(self): 19 | valid = super(BaseForm, self).validate() 20 | if not valid: 21 | raise ParameterException(msg=self.errors) 22 | return self 23 | -------------------------------------------------------------------------------- /app/validators/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # @Time : 2020/2/3 5:49 下午 3 | # @Author : luYuZe 4 | # @File : forms.py 5 | # @Project : ginger 6 | 7 | from wtforms import StringField, IntegerField 8 | from wtforms.validators import DataRequired, length, Email, Regexp, ValidationError 9 | 10 | from app.libs.enums import ClientTypeEnum 11 | from app.models.user import User 12 | from app.validators.base import BaseForm as Form 13 | 14 | 15 | class ClientForm(Form): 16 | account = StringField(validators=[DataRequired(message='account is required'), length(min=5, max=32)]) 17 | secret = StringField() 18 | type = IntegerField(validators=[DataRequired()]) 19 | 20 | def validate_type(self, value): 21 | try: 22 | client = ClientTypeEnum(value.data) 23 | except ValueError as e: 24 | raise e 25 | self.type.data = client 26 | 27 | 28 | class UserEmailForm(ClientForm): 29 | account = StringField(validators=[Email(message='invalidate email')]) 30 | secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')]) 31 | nickname = StringField(validators=[DataRequired(), length(min=2, max=22)]) 32 | 33 | def validate_account(self, value): 34 | if User.query.filter_by(email=value.data).first(): 35 | raise ValidationError() 36 | -------------------------------------------------------------------------------- /code.md: -------------------------------------------------------------------------------- 1 | 999 unknown error 2 | 1006 client is invalid -------------------------------------------------------------------------------- /fake.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created by 七月 on 2018/5/1. 3 | """ 4 | __author__ = '七月' 5 | 6 | 7 | from app import create_app 8 | from app.models.base import db 9 | from app.models.user import User 10 | 11 | app = create_app() 12 | with app.app_context(): 13 | with db.auto_commit(): 14 | # 创建一个超级管理员 15 | user = User() 16 | user.nickname = 'Super' 17 | user.password = '123456' 18 | user.email = '999@qq.com' 19 | user.auth = 2 20 | db.session.add(user) 21 | -------------------------------------------------------------------------------- /ginger.py: -------------------------------------------------------------------------------- 1 | from werkzeug.exceptions import HTTPException 2 | 3 | from app import create_app 4 | from app.libs.error import APIException 5 | from app.libs.error_code import ServerError 6 | 7 | app = create_app() 8 | 9 | # 错误处理 10 | @app.errorhandler(Exception) 11 | def framework_error(e): 12 | if isinstance(e, APIException): 13 | return e 14 | if isinstance(e, HTTPException): 15 | code = e.code 16 | msg = e.description 17 | error_code = 1007 18 | return APIException(msg, code, error_code) 19 | else: 20 | if not app.config['DEBUG']: 21 | return ServerError() 22 | else: 23 | return e 24 | 25 | 26 | if __name__ == '__main__': 27 | app.run(debug=True) 28 | --------------------------------------------------------------------------------