From 42f8d6f7e5e0400eb994170867ce59301ed23660 Mon Sep 17 00:00:00 2001 From: Btapple Date: Fri, 10 Apr 2020 18:37:13 +0800 Subject: [PATCH 1/6] auth impl and project demo (#1) * auth impl and project demo * method restricted and url filled --- auth/api/__init__.py | 0 auth/api/auth.py | 98 +++++++++++++++++++++++++++++++++ auth/api/user_management.py | 35 ++++++++++++ auth/models.py | 3 - auth/models/__init__.py | 0 auth/models/permission_level.py | 17 ++++++ auth/tests.py | 3 - auth/urls.py | 7 +++ auth/views.py | 3 - common/__init__.py | 0 common/consts.py | 36 ++++++++++++ common/utils.py | 53 ++++++++++++++++++ nocode_backend/settings.py | 2 +- nocode_backend/urls.py | 3 +- requirements.txt | 3 +- 15 files changed, 251 insertions(+), 12 deletions(-) create mode 100644 auth/api/__init__.py create mode 100644 auth/api/auth.py create mode 100644 auth/api/user_management.py delete mode 100644 auth/models.py create mode 100644 auth/models/__init__.py create mode 100644 auth/models/permission_level.py delete mode 100644 auth/tests.py create mode 100644 auth/urls.py delete mode 100644 auth/views.py create mode 100644 common/__init__.py create mode 100644 common/consts.py create mode 100644 common/utils.py diff --git a/auth/api/__init__.py b/auth/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/api/auth.py b/auth/api/auth.py new file mode 100644 index 0000000..226b885 --- /dev/null +++ b/auth/api/auth.py @@ -0,0 +1,98 @@ +from datetime import timedelta + +import jwt +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.db.models import Q +from django.http import HttpRequest +from django.utils import timezone +from django.views.decorators.http import require_http_methods + +from auth.models.permission_level import UserPermission +from common.consts import Everyone, User +from common.utils import (StatusCode, failed_api_response, response_wrapper, + success_api_response) + +UserModel = get_user_model() + + +def auth_failed(status: StatusCode, msg: str): + return failed_api_response(status, msg) + + +def generate_access_token(user_id: int, valid_hours: int = 24) -> str: + current_time = timezone.now() + access_token_payload = { + "user_id": user_id, + "exp": current_time + timedelta(hours=valid_hours), + "iat": current_time, + "type": "access_token", + } + return jwt.encode(access_token_payload, settings.SECRET_KEY, algorithm="HS256").decode("utf-8") + + +@response_wrapper +@require_http_methods(["POST"]) +def login(request: HttpRequest): + """Handle requests which are to obtain jwt token + + [route]: /auth + + [method]: POST + """ + user = authenticate(username=request.POST.get( + "username"), password=request.POST.get("password")) + if not user: + return failed_api_response(StatusCode.INVALID_USERNAME_OR_PASSWORD, "Login required") + return success_api_response({ + "access_token": generate_access_token(user.id) + }) + + +def verify_jwt_token(request: HttpRequest) -> (bool, StatusCode, str, int): + flag: bool = True + msg: str = "" + user_id: int = -1 + status: StatusCode = StatusCode.SUCCESS + header: str = request.META.get("HTTP_AUTHORIZATION") + try: + if header is None: + raise jwt.InvalidTokenError + + auth_info = header.split(" ") + if len(auth_info) != 2: + raise jwt.InvalidTokenError + auth_type, auth_token = auth_info + + if auth_type != "Bearer": + raise jwt.InvalidTokenError + token = jwt.decode(auth_token, settings.SECRET_KEY, algorithms="HS256") + if token.get("type") != "access_token": + raise jwt.InvalidTokenError + user_id = int(token["user_id"]) + except jwt.ExpiredSignatureError: + flag, status, msg = False, StatusCode.TOKEN_EXPIRED, "Token expired" + except jwt.InvalidTokenError: + flag, status, msg = False, StatusCode.INVALID_TOKEN, "Invalid token" + return (flag, status, msg, user_id) + + +def auth_required(level: int): + def decorator(func): + def wrapper(request: HttpRequest, *args, **kwargs): + if level != Everyone: + (flag, status, msg, user_id) = verify_jwt_token(request) + if not flag: + return auth_failed(status, msg) + request_user = UserModel.objects.filter(pk=user_id).first() + if request_user is None: + return auth_failed(StatusCode.ACCOUNT_DISABLED, "Sorry, your account has been disabled") + request.user = request_user + if level > User and not UserPermission.objects.filter(Q(user__id=user_id) & + Q(level__gte=level) & + Q(created_at__lt=timezone.now()) & + Q(expires_at__gt=timezone.now())).exists(): + return auth_failed(StatusCode.PERMISSION_DENIED, "Permission denied") + return func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/auth/api/user_management.py b/auth/api/user_management.py new file mode 100644 index 0000000..1d95e95 --- /dev/null +++ b/auth/api/user_management.py @@ -0,0 +1,35 @@ +from django.contrib.auth import get_user_model +from django.http import HttpRequest +from django.views.decorators.http import require_http_methods + +from common.consts import StatusCode +from common.utils import (failed_api_response, parse_data, response_wrapper, + success_api_response) + +UserModel = get_user_model() + + +@response_wrapper +@require_http_methods(["POST"]) +def create_user(request: HttpRequest): + """create user + + [route]: /auth/create + + [method]: POST + """ + user_info: dict = parse_data(request) + if not user_info: + return failed_api_response(StatusCode.BAD_REQUEST, "Bad request") + username = user_info.get("username") + password = user_info.get("password") + email = user_info.get("email") + if username is None or password is None or email is None: + return failed_api_response(StatusCode.INVALID_REQUEST_ARGUMENT, "Bad user information") + if UserModel.objects.filter(username=username).exists(): + return failed_api_response(StatusCode.ITEM_ALREADY_EXISTS, "Username conflicted") + + new_user = UserModel.objects.create_user( + username=username, password=password, email=email) + + return success_api_response({"id": new_user.id}) diff --git a/auth/models.py b/auth/models.py deleted file mode 100644 index fd18c6e..0000000 --- a/auth/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/auth/models/__init__.py b/auth/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auth/models/permission_level.py b/auth/models/permission_level.py new file mode 100644 index 0000000..bc6bbcc --- /dev/null +++ b/auth/models/permission_level.py @@ -0,0 +1,17 @@ +from django.contrib.auth import get_user_model +from django.db import models +from common.consts import Everyone, User, VIP, Admin + + +class UserPermission(models.Model): + level = [ + (Everyone, "Everyone can access"), + (User, "Logined user can access"), + (VIP, "VIP user can access"), + (Admin, "Only administrators can access") + ] + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + level = models.IntegerField(choices=level, default=Everyone) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() diff --git a/auth/tests.py b/auth/tests.py deleted file mode 100644 index de8bdc0..0000000 --- a/auth/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/auth/urls.py b/auth/urls.py new file mode 100644 index 0000000..34b4cfa --- /dev/null +++ b/auth/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from auth.api.auth import login +from auth.api.user_management import create_user +urlpatterns = [ + path("", login), + path("create", create_user) +] diff --git a/auth/views.py b/auth/views.py deleted file mode 100644 index c60c790..0000000 --- a/auth/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/common/consts.py b/common/consts.py new file mode 100644 index 0000000..07ad591 --- /dev/null +++ b/common/consts.py @@ -0,0 +1,36 @@ +# Permission Levels +Everyone = 0 +User = 1 +VIP = 2 +Admin = 3 + +# HTTP Status Code + + +class StatusCode(Enum): + # Success Code Family + SUCCESS = 200_00 + + # Bad Request Family + BAD_REQUEST = 400_00 + INVALID_REQUEST_ARGUMENT = 400_01 + + # Unauthorized Family + UNAUTHORIZED = 401_00 + INVALID_USERNAME_OR_PASSWORD = 401_01 + INVALID_TOKEN = 401_02 + TOKEN_EXPIRED = 401_03 + ACCOUNT_DISABLED = 401_04 + + # Refuse Access Family + REFUSE_ACCESS = 403_00 + PERMISSION_DENIED = 403_01 + + # Not Found Family + ITEM_NOT_FOUND = 404_00 + + # Resource Too Huge Family + Image_HUGE = 413_00 + + # Duplicated Family + ITEM_ALREADY_EXISTS = 409_00 diff --git a/common/utils.py b/common/utils.py new file mode 100644 index 0000000..86d45dc --- /dev/null +++ b/common/utils.py @@ -0,0 +1,53 @@ +import json +from enum import Enum + +from django.http import HttpRequest, JsonResponse +from django.views.decorators.http import require_http_methods + +from common.consts import StatusCode + + +def response_wrapper(func): + def inner(*args, **kwargs): + response = func(*args, **kwargs) + if isinstance(response, dict): + if response["success"]: + response = JsonResponse(response["data"]) + else: + status_code = response.get("data").get("code") + response = JsonResponse(response["data"]) + response.status_code = status_code + return response + + return inner + + +def api_response(success, data) -> dict: + return {"success": success, "data": data} + + +def failed_api_response(code: StatusCode, error_msg=None) -> dict: + if error_msg is None: + error_msg = str(code) + else: + error_msg = str(code) + ": " + error_msg + + status_code = code.value // 100 + detailed_code = code.value + return api_response( + success=False, + data={ + "code": status_code, + "detailed_error_code": detailed_code, + "error_msg": error_msg + }) + + +def success_api_response(data) -> dict: + return api_response(True, data) + +def parse_data(request: HttpRequest): + try: + return json.loads(request.body.decode()) + except json.JSONDecodeError: + return None diff --git a/nocode_backend/settings.py b/nocode_backend/settings.py index fd0072d..a6ac546 100644 --- a/nocode_backend/settings.py +++ b/nocode_backend/settings.py @@ -105,7 +105,7 @@ LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' +TIME_ZONE = 'Asia/Shanghai' USE_I18N = True diff --git a/nocode_backend/urls.py b/nocode_backend/urls.py index 736875e..75f689a 100644 --- a/nocode_backend/urls.py +++ b/nocode_backend/urls.py @@ -14,8 +14,9 @@ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), + path('auth/', include('auth.urls')), ] diff --git a/requirements.txt b/requirements.txt index f5aa045..9f6d09c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django==3.0.5 coverage==5.0.4 psycopg2 -requests \ No newline at end of file +requests +pyjwt \ No newline at end of file From c39f166af9171f893a745660c5ab1a5e322c20c5 Mon Sep 17 00:00:00 2001 From: Btapple Date: Sat, 11 Apr 2020 14:14:51 +0800 Subject: [PATCH 2/6] docs(docs): create docs demo (#2) auth docs as demo --- docs/api/auth.md | 69 +++++++++++++++++++++++++++++++++++++++++++ docs/database/auth.md | 0 2 files changed, 69 insertions(+) create mode 100644 docs/api/auth.md create mode 100644 docs/database/auth.md diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..22981f8 --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,69 @@ +# auth 模块 API + +## 总览 + +以下表格列出了与用户认证相关的 API: + +| 方法 | 路径 | 描述 | Response | +| ------ | ------------------------ | -------- | ------------ | +| `POST` | [`/auth`](#post-auth) | 用户登录 | access token | +| `POST` | [`/auth/create`](#post-) | 用户创建 | 用户 ID | + +## 详细描述 + +### `POST /auth` + +使用用户名和密码进行登录,后端生成`access_token`。 + +后续请求需要带上`access_token`用于鉴权。 + +如果鉴权得到`access_token`过期的错误信息,将需要重新登录获取新的`access_token`。 + +#### Request Body + +**必要字段** + +| 字段 | 类型 | 描述 | +| :------: | :----: | :----: | +| username | String | 用户名 | +| password | String | 密码 | + +#### 请求示例 + +```bash +curl -X POST \ + http://127.0.0.1:8000/auth \ + -F username=test \ + -F password=test +``` + +#### Response Body + +| 字段 | 类型 | 描述 | +| :----------: | :----: | :-------: | +| access_token | String | JWT Token | + +```json +// Example +{ + "access_token": "aaa.bbb.ccc" +} +``` + +### `POST /auth/create` + +#### Request Body + +**必要字段** + +| 字段 | 类型 | 描述 | +| :------: | :----: | :------: | +| username | String | 用户名 | +| password | String | 密码 | +| email | String | 电邮地址 | + +#### Response Body + +| 字段 | 类型 | 描述 | +| :--: | :-----: | :-----: | +| id | Integer | 用户 ID | diff --git a/docs/database/auth.md b/docs/database/auth.md new file mode 100644 index 0000000..e69de29 From 633ae6924212f3d82e96bdf0eaef17763b099e06 Mon Sep 17 00:00:00 2001 From: Btapple Date: Tue, 14 Apr 2020 19:57:58 +0800 Subject: [PATCH 3/6] init ocr app (#3) --- ocr/models.py | 3 --- ocr/models/__init__.py | 0 ocr/models/project.py | 9 +++++++++ ocr/models/recognition_result.py | 9 +++++++++ ocr/tests.py | 3 --- ocr/urls.py | 0 ocr/views.py | 3 --- 7 files changed, 18 insertions(+), 9 deletions(-) delete mode 100644 ocr/models.py create mode 100644 ocr/models/__init__.py create mode 100644 ocr/models/project.py create mode 100644 ocr/models/recognition_result.py delete mode 100644 ocr/tests.py create mode 100644 ocr/urls.py delete mode 100644 ocr/views.py diff --git a/ocr/models.py b/ocr/models.py deleted file mode 100644 index fd18c6e..0000000 --- a/ocr/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/ocr/models/__init__.py b/ocr/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ocr/models/project.py b/ocr/models/project.py new file mode 100644 index 0000000..c7a819e --- /dev/null +++ b/ocr/models/project.py @@ -0,0 +1,9 @@ +from django.db import models +from django.contrib.auth import get_user_model + +UserModel = get_user_model() + + +class Project(models.Model): + belong_to = models.ForeignKey(to=UserModel, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) diff --git a/ocr/models/recognition_result.py b/ocr/models/recognition_result.py new file mode 100644 index 0000000..55f0622 --- /dev/null +++ b/ocr/models/recognition_result.py @@ -0,0 +1,9 @@ +from django.db import models +from django.contrib.auth import get_user_model +from django.contrib.postgres.fields import JSONField +from ocr.models.project import Project + +class RecognitionResult(models.Model): + belong_to = models.ForeignKey(to=Project, on_delete=models.CASCADE) + result = JSONField() + created_at = models.DateTimeField(auto_now_add=True) diff --git a/ocr/tests.py b/ocr/tests.py deleted file mode 100644 index de8bdc0..0000000 --- a/ocr/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/ocr/urls.py b/ocr/urls.py new file mode 100644 index 0000000..e69de29 diff --git a/ocr/views.py b/ocr/views.py deleted file mode 100644 index c60c790..0000000 --- a/ocr/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. From b57df897eb16bee229a7057ab8f161eaaffae5b8 Mon Sep 17 00:00:00 2001 From: btapple Date: Wed, 15 Apr 2020 10:26:29 +0800 Subject: [PATCH 4/6] add info columns to ocr/models and init ocr docs --- docs/api/ocr.md | 163 +++++++++++++++++++++++++++++++ ocr/api/__init__.py | 0 ocr/api/project.py | 0 ocr/models/project.py | 2 + ocr/models/recognition_result.py | 2 + 5 files changed, 167 insertions(+) create mode 100644 docs/api/ocr.md create mode 100644 ocr/api/__init__.py create mode 100644 ocr/api/project.py diff --git a/docs/api/ocr.md b/docs/api/ocr.md new file mode 100644 index 0000000..159d257 --- /dev/null +++ b/docs/api/ocr.md @@ -0,0 +1,163 @@ +# ocr 模块 API + +## 总览 + +以下表格列出了与 OCR 识别相关的 API: + +### project + +| 方法 | 路径 | 描述 | +| -------- | ----------------------------------------------------------------- | ---------------- | +| `POST` | [`/ocr/project`](#post-ocrproject) | 创建项目 | +| `PUT` | [`/ocr/project/`](#put-ocrprojectintprojectid) | 修改项目信息 | +| `GET` | [`/ocr/project`](#get-ocrproject) | 获取所有项目信息 | +| `GET` | [`/ocr/project/`](#get-ocrprojectintprojectid) | 获取项目详细信息 | +| `DELETE` | [`/ocr/project/`](#delete-ocrprojectintprojectid) | 删除项目 | + +### recognition_result + +| 方法 | 路径 | 描述 | +| -------- | -------------------------------------------------------------------------------------------- | ---------------- | +| `POST` | [`/ocr/project/`](#post-ocrprojectintprojectid) | 上传待识别图片 | +| `GET` | [`/ocr/project//`](#get-ocrprojectintprojectidintresultid) | 获取识别结果 | +| `PUT` | [`/ocr/project//`](#put-ocrprojectintprojectidintresultid) | 修改识别结果信息 | +| `DELETE` | [`/ocr/project//`](#delete-ocrprojectintprojectidintresultid) | 删除识别结果 | + +## 详细描述 + +### `POST /ocr/project` + +创建项目 + +#### Request Body + +| 字段 | 类型 | 描述 | +| :-----: | :----: | :------------: | +| name | String | 项目名 | +| comment | String | 可选,项目描述 | + +#### Response Body + +| 字段 | 类型 | 描述 | +| :--------: | :------: | :----------: | +| id | Integer | 项目 ID | +| created_at | DateTime | 项目创建时间 | + +### `PUT /ocr/project/` + +修改项目信息 + +#### Request Body + +| 字段 | 类型 | 描述 | +| :-----: | :----: | :------------: | +| name | String | 可选,项目名 | +| comment | String | 可选,项目描述 | + +### `GET /ocr/project` + +获取该用户所有项目信息 + +#### Response Body + +| 字段 | 类型 | 描述 | +| :---------: | :-----: | :----------: | +| projects | Array | 所有项目信息 | +| project_num | Integer | 项目总数 | + +projects 字段元素内容 + +| 字段 | 类型 | 描述 | +| :--------: | :------: | :----------: | +| name | String | 项目名 | +| comment | String | 项目描述 | +| created_at | DateTime | 创建时间 | +| result_num | Integer | 识别结果总数 | + +### `GET /ocr/project/` + +获取项目详细信息 + +#### Response Body + +| 字段 | 类型 | 描述 | +| :--------: | :------: | :----------: | +| name | String | 项目名 | +| comment | String | 项目描述 | +| created_at | DateTime | 创建时间 | +| result_num | Integer | 识别结果总数 | +| results | Array | 所有识别结果 | + +results 字段元素内容 + +| 字段 | 类型 | 描述 | +| :--------: | :------: | :----------: | +| name | String | 识别结果名 | +| comment | String | 识别结果描述 | +| created_at | DateTime | 创建时间 | + +### `DELETE /ocr/project/` + +删除项目 + +### `POST /ocr/project/` + +上传待识别图片 + +#### Request Body(JSON part) + +| 字段 | 类型 | 描述 | +| :-----: | :----: | :----------------: | +| name | String | 可选,识别结果名 | +| comment | String | 可选,识别结果描述 | + +可能的发包方法 + +```python +import json +import requests + +payload = { + "name": "test", + "comment": "test comment" +} + +files = { + "json": (None, json.dumps(payload), "application/json"), + "file": (os.path.basename(file), open(file, "rb"), "image/png") +} + +requests.post(url, files=files) +``` + +#### Response Body + +| 字段 | 类型 | 描述 | +| :--------: | :------: | :---------: | +| id | Integer | 识别结果 ID | +| created_at | DateTime | 创建时间 | + +### `GET /ocr/project//` + +获取识别结果 + +#### Response Body + +| 字段 | 类型 | 描述 | +| :-----: | :----: | :----------: | +| name | String | 识别结果名 | +| comment | String | 识别结果描述 | +| result | JSON | 识别结果 | + +### `PUT /ocr/project//` + +修改识别结果信息 + +| 字段 | 类型 | 描述 | +| :-----: | :----: | :----------------: | +| name | String | 可选,识别结果名 | +| comment | String | 可选,识别结果描述 | + +### `DELETE /ocr/project//` + +删除识别结果 diff --git a/ocr/api/__init__.py b/ocr/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ocr/api/project.py b/ocr/api/project.py new file mode 100644 index 0000000..e69de29 diff --git a/ocr/models/project.py b/ocr/models/project.py index c7a819e..43a2cbf 100644 --- a/ocr/models/project.py +++ b/ocr/models/project.py @@ -5,5 +5,7 @@ class Project(models.Model): + name = models.CharField(max_length=50, default="") + comment = models.TextField(default="") belong_to = models.ForeignKey(to=UserModel, on_delete=models.CASCADE) created_at = models.DateTimeField(auto_now_add=True) diff --git a/ocr/models/recognition_result.py b/ocr/models/recognition_result.py index 55f0622..bfd22cf 100644 --- a/ocr/models/recognition_result.py +++ b/ocr/models/recognition_result.py @@ -4,6 +4,8 @@ from ocr.models.project import Project class RecognitionResult(models.Model): + name = models.CharField(max_length=50, default="") + comment = models.TextField(default="") belong_to = models.ForeignKey(to=Project, on_delete=models.CASCADE) result = JSONField() created_at = models.DateTimeField(auto_now_add=True) From db1240d4a96864c988fe1766efe696e8275944e2 Mon Sep 17 00:00:00 2001 From: yankuai Date: Sat, 18 Apr 2020 00:41:33 +0800 Subject: [PATCH 5/6] rename auth --- auth/apps.py | 5 - auth/urls.py | 7 - common/consts.py | 1 + nocode_backend/settings.py | 1 + nocode_backend/urls.py | 2 +- {auth => userManagement}/__init__.py | 0 {auth => userManagement}/admin.py | 6 +- {auth => userManagement}/api/__init__.py | 0 {auth => userManagement}/api/auth.py | 196 +++++++++--------- .../api/user_management.py | 70 +++---- userManagement/apps.py | 5 + userManagement/migrations/0001_initial.py | 27 +++ .../migrations/__init__.py | 0 userManagement/models.py | 3 + {auth => userManagement}/models/__init__.py | 0 .../models/permission_level.py | 34 +-- userManagement/tests.py | 3 + userManagement/urls.py | 7 + userManagement/views.py | 3 + 19 files changed, 204 insertions(+), 166 deletions(-) delete mode 100644 auth/apps.py delete mode 100644 auth/urls.py rename {auth => userManagement}/__init__.py (100%) rename {auth => userManagement}/admin.py (95%) rename {auth => userManagement}/api/__init__.py (100%) rename {auth => userManagement}/api/auth.py (95%) rename {auth => userManagement}/api/user_management.py (97%) create mode 100644 userManagement/apps.py create mode 100644 userManagement/migrations/0001_initial.py rename {auth => userManagement}/migrations/__init__.py (100%) create mode 100644 userManagement/models.py rename {auth => userManagement}/models/__init__.py (100%) rename {auth => userManagement}/models/permission_level.py (97%) create mode 100644 userManagement/tests.py create mode 100644 userManagement/urls.py create mode 100644 userManagement/views.py diff --git a/auth/apps.py b/auth/apps.py deleted file mode 100644 index cee79fc..0000000 --- a/auth/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class AuthConfig(AppConfig): - name = 'auth' diff --git a/auth/urls.py b/auth/urls.py deleted file mode 100644 index 34b4cfa..0000000 --- a/auth/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.urls import path -from auth.api.auth import login -from auth.api.user_management import create_user -urlpatterns = [ - path("", login), - path("create", create_user) -] diff --git a/common/consts.py b/common/consts.py index 07ad591..d44088d 100644 --- a/common/consts.py +++ b/common/consts.py @@ -1,3 +1,4 @@ +from enum import Enum # Permission Levels Everyone = 0 User = 1 diff --git a/nocode_backend/settings.py b/nocode_backend/settings.py index a6ac546..2e62833 100644 --- a/nocode_backend/settings.py +++ b/nocode_backend/settings.py @@ -31,6 +31,7 @@ # Application definition INSTALLED_APPS = [ + 'userManagement.apps.UsermanagementConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/nocode_backend/urls.py b/nocode_backend/urls.py index 75f689a..dbea088 100644 --- a/nocode_backend/urls.py +++ b/nocode_backend/urls.py @@ -18,5 +18,5 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('auth/', include('auth.urls')), + path('userManagement/', include('userManagement.urls')), ] diff --git a/auth/__init__.py b/userManagement/__init__.py similarity index 100% rename from auth/__init__.py rename to userManagement/__init__.py diff --git a/auth/admin.py b/userManagement/admin.py similarity index 95% rename from auth/admin.py rename to userManagement/admin.py index ea5d68b..8c38f3f 100644 --- a/auth/admin.py +++ b/userManagement/admin.py @@ -1,3 +1,3 @@ -from django.contrib import admin - -# Register your models here. +from django.contrib import admin + +# Register your models here. diff --git a/auth/api/__init__.py b/userManagement/api/__init__.py similarity index 100% rename from auth/api/__init__.py rename to userManagement/api/__init__.py diff --git a/auth/api/auth.py b/userManagement/api/auth.py similarity index 95% rename from auth/api/auth.py rename to userManagement/api/auth.py index 226b885..38006f0 100644 --- a/auth/api/auth.py +++ b/userManagement/api/auth.py @@ -1,98 +1,98 @@ -from datetime import timedelta - -import jwt -from django.conf import settings -from django.contrib.auth import authenticate, get_user_model -from django.db.models import Q -from django.http import HttpRequest -from django.utils import timezone -from django.views.decorators.http import require_http_methods - -from auth.models.permission_level import UserPermission -from common.consts import Everyone, User -from common.utils import (StatusCode, failed_api_response, response_wrapper, - success_api_response) - -UserModel = get_user_model() - - -def auth_failed(status: StatusCode, msg: str): - return failed_api_response(status, msg) - - -def generate_access_token(user_id: int, valid_hours: int = 24) -> str: - current_time = timezone.now() - access_token_payload = { - "user_id": user_id, - "exp": current_time + timedelta(hours=valid_hours), - "iat": current_time, - "type": "access_token", - } - return jwt.encode(access_token_payload, settings.SECRET_KEY, algorithm="HS256").decode("utf-8") - - -@response_wrapper -@require_http_methods(["POST"]) -def login(request: HttpRequest): - """Handle requests which are to obtain jwt token - - [route]: /auth - - [method]: POST - """ - user = authenticate(username=request.POST.get( - "username"), password=request.POST.get("password")) - if not user: - return failed_api_response(StatusCode.INVALID_USERNAME_OR_PASSWORD, "Login required") - return success_api_response({ - "access_token": generate_access_token(user.id) - }) - - -def verify_jwt_token(request: HttpRequest) -> (bool, StatusCode, str, int): - flag: bool = True - msg: str = "" - user_id: int = -1 - status: StatusCode = StatusCode.SUCCESS - header: str = request.META.get("HTTP_AUTHORIZATION") - try: - if header is None: - raise jwt.InvalidTokenError - - auth_info = header.split(" ") - if len(auth_info) != 2: - raise jwt.InvalidTokenError - auth_type, auth_token = auth_info - - if auth_type != "Bearer": - raise jwt.InvalidTokenError - token = jwt.decode(auth_token, settings.SECRET_KEY, algorithms="HS256") - if token.get("type") != "access_token": - raise jwt.InvalidTokenError - user_id = int(token["user_id"]) - except jwt.ExpiredSignatureError: - flag, status, msg = False, StatusCode.TOKEN_EXPIRED, "Token expired" - except jwt.InvalidTokenError: - flag, status, msg = False, StatusCode.INVALID_TOKEN, "Invalid token" - return (flag, status, msg, user_id) - - -def auth_required(level: int): - def decorator(func): - def wrapper(request: HttpRequest, *args, **kwargs): - if level != Everyone: - (flag, status, msg, user_id) = verify_jwt_token(request) - if not flag: - return auth_failed(status, msg) - request_user = UserModel.objects.filter(pk=user_id).first() - if request_user is None: - return auth_failed(StatusCode.ACCOUNT_DISABLED, "Sorry, your account has been disabled") - request.user = request_user - if level > User and not UserPermission.objects.filter(Q(user__id=user_id) & - Q(level__gte=level) & - Q(created_at__lt=timezone.now()) & - Q(expires_at__gt=timezone.now())).exists(): - return auth_failed(StatusCode.PERMISSION_DENIED, "Permission denied") - return func(request, *args, **kwargs) - return wrapper - return decorator +from datetime import timedelta + +import jwt +from django.conf import settings +from django.contrib.auth import authenticate, get_user_model +from django.db.models import Q +from django.http import HttpRequest +from django.utils import timezone +from django.views.decorators.http import require_http_methods + +from userManagement.models.permission_level import UserPermission +from common.consts import Everyone, User +from common.utils import (StatusCode, failed_api_response, response_wrapper, + success_api_response) + +UserModel = get_user_model() + + +def auth_failed(status: StatusCode, msg: str): + return failed_api_response(status, msg) + + +def generate_access_token(user_id: int, valid_hours: int = 24) -> str: + current_time = timezone.now() + access_token_payload = { + "user_id": user_id, + "exp": current_time + timedelta(hours=valid_hours), + "iat": current_time, + "type": "access_token", + } + return jwt.encode(access_token_payload, settings.SECRET_KEY, algorithm="HS256").decode("utf-8") + + +@response_wrapper +@require_http_methods(["POST"]) +def login(request: HttpRequest): + """Handle requests which are to obtain jwt token + + [route]: /auth + + [method]: POST + """ + user = authenticate(username=request.POST.get( + "username"), password=request.POST.get("password")) + if not user: + return failed_api_response(StatusCode.INVALID_USERNAME_OR_PASSWORD, "Login required") + return success_api_response({ + "access_token": generate_access_token(user.id) + }) + + +def verify_jwt_token(request: HttpRequest) -> (bool, StatusCode, str, int): + flag: bool = True + msg: str = "" + user_id: int = -1 + status: StatusCode = StatusCode.SUCCESS + header: str = request.META.get("HTTP_AUTHORIZATION") + try: + if header is None: + raise jwt.InvalidTokenError + + auth_info = header.split(" ") + if len(auth_info) != 2: + raise jwt.InvalidTokenError + auth_type, auth_token = auth_info + + if auth_type != "Bearer": + raise jwt.InvalidTokenError + token = jwt.decode(auth_token, settings.SECRET_KEY, algorithms="HS256") + if token.get("type") != "access_token": + raise jwt.InvalidTokenError + user_id = int(token["user_id"]) + except jwt.ExpiredSignatureError: + flag, status, msg = False, StatusCode.TOKEN_EXPIRED, "Token expired" + except jwt.InvalidTokenError: + flag, status, msg = False, StatusCode.INVALID_TOKEN, "Invalid token" + return (flag, status, msg, user_id) + + +def auth_required(level: int): + def decorator(func): + def wrapper(request: HttpRequest, *args, **kwargs): + if level != Everyone: + (flag, status, msg, user_id) = verify_jwt_token(request) + if not flag: + return auth_failed(status, msg) + request_user = UserModel.objects.filter(pk=user_id).first() + if request_user is None: + return auth_failed(StatusCode.ACCOUNT_DISABLED, "Sorry, your account has been disabled") + request.user = request_user + if level > User and not UserPermission.objects.filter(Q(user__id=user_id) & + Q(level__gte=level) & + Q(created_at__lt=timezone.now()) & + Q(expires_at__gt=timezone.now())).exists(): + return auth_failed(StatusCode.PERMISSION_DENIED, "Permission denied") + return func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/auth/api/user_management.py b/userManagement/api/user_management.py similarity index 97% rename from auth/api/user_management.py rename to userManagement/api/user_management.py index 1d95e95..39c86be 100644 --- a/auth/api/user_management.py +++ b/userManagement/api/user_management.py @@ -1,35 +1,35 @@ -from django.contrib.auth import get_user_model -from django.http import HttpRequest -from django.views.decorators.http import require_http_methods - -from common.consts import StatusCode -from common.utils import (failed_api_response, parse_data, response_wrapper, - success_api_response) - -UserModel = get_user_model() - - -@response_wrapper -@require_http_methods(["POST"]) -def create_user(request: HttpRequest): - """create user - - [route]: /auth/create - - [method]: POST - """ - user_info: dict = parse_data(request) - if not user_info: - return failed_api_response(StatusCode.BAD_REQUEST, "Bad request") - username = user_info.get("username") - password = user_info.get("password") - email = user_info.get("email") - if username is None or password is None or email is None: - return failed_api_response(StatusCode.INVALID_REQUEST_ARGUMENT, "Bad user information") - if UserModel.objects.filter(username=username).exists(): - return failed_api_response(StatusCode.ITEM_ALREADY_EXISTS, "Username conflicted") - - new_user = UserModel.objects.create_user( - username=username, password=password, email=email) - - return success_api_response({"id": new_user.id}) +from django.contrib.auth import get_user_model +from django.http import HttpRequest +from django.views.decorators.http import require_http_methods + +from common.consts import StatusCode +from common.utils import (failed_api_response, parse_data, response_wrapper, + success_api_response) + +UserModel = get_user_model() + + +@response_wrapper +@require_http_methods(["POST"]) +def create_user(request: HttpRequest): + """create user + + [route]: /auth/create + + [method]: POST + """ + user_info: dict = parse_data(request) + if not user_info: + return failed_api_response(StatusCode.BAD_REQUEST, "Bad request") + username = user_info.get("username") + password = user_info.get("password") + email = user_info.get("email") + if username is None or password is None or email is None: + return failed_api_response(StatusCode.INVALID_REQUEST_ARGUMENT, "Bad user information") + if UserModel.objects.filter(username=username).exists(): + return failed_api_response(StatusCode.ITEM_ALREADY_EXISTS, "Username conflicted") + + new_user = UserModel.objects.create_user( + username=username, password=password, email=email) + + return success_api_response({"id": new_user.id}) diff --git a/userManagement/apps.py b/userManagement/apps.py new file mode 100644 index 0000000..f191239 --- /dev/null +++ b/userManagement/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class UsermanagementConfig(AppConfig): + name = 'userManagement' diff --git a/userManagement/migrations/0001_initial.py b/userManagement/migrations/0001_initial.py new file mode 100644 index 0000000..e072187 --- /dev/null +++ b/userManagement/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.5 on 2020-04-17 16:32 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserPermission', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.IntegerField(choices=[(0, 'Everyone can access'), (1, 'Logined user can access'), (2, 'VIP user can access'), (3, 'Only administrators can access')], default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('expires_at', models.DateTimeField()), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/auth/migrations/__init__.py b/userManagement/migrations/__init__.py similarity index 100% rename from auth/migrations/__init__.py rename to userManagement/migrations/__init__.py diff --git a/userManagement/models.py b/userManagement/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/userManagement/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/auth/models/__init__.py b/userManagement/models/__init__.py similarity index 100% rename from auth/models/__init__.py rename to userManagement/models/__init__.py diff --git a/auth/models/permission_level.py b/userManagement/models/permission_level.py similarity index 97% rename from auth/models/permission_level.py rename to userManagement/models/permission_level.py index bc6bbcc..a16b6d9 100644 --- a/auth/models/permission_level.py +++ b/userManagement/models/permission_level.py @@ -1,17 +1,17 @@ -from django.contrib.auth import get_user_model -from django.db import models -from common.consts import Everyone, User, VIP, Admin - - -class UserPermission(models.Model): - level = [ - (Everyone, "Everyone can access"), - (User, "Logined user can access"), - (VIP, "VIP user can access"), - (Admin, "Only administrators can access") - ] - - user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - level = models.IntegerField(choices=level, default=Everyone) - created_at = models.DateTimeField(auto_now_add=True) - expires_at = models.DateTimeField() +from django.contrib.auth import get_user_model +from django.db import models +from common.consts import Everyone, User, VIP, Admin + + +class UserPermission(models.Model): + level = [ + (Everyone, "Everyone can access"), + (User, "Logined user can access"), + (VIP, "VIP user can access"), + (Admin, "Only administrators can access") + ] + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + level = models.IntegerField(choices=level, default=Everyone) + created_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField() diff --git a/userManagement/tests.py b/userManagement/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/userManagement/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/userManagement/urls.py b/userManagement/urls.py new file mode 100644 index 0000000..9bde1f4 --- /dev/null +++ b/userManagement/urls.py @@ -0,0 +1,7 @@ +from django.urls import path +from userManagement.api.auth import login +from userManagement.api.user_management import create_user +urlpatterns = [ + path("", login), + path("create", create_user) +] diff --git a/userManagement/views.py b/userManagement/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/userManagement/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. From ac661b00f1e106bc077a5bfcf145434a31684273 Mon Sep 17 00:00:00 2001 From: yankuai Date: Sun, 19 Apr 2020 19:59:24 +0800 Subject: [PATCH 6/6] api --- docs/api/ocr.md | 1 + nocode_backend/settings.py | 1 + nocode_backend/urls.py | 1 + ocr/urls.py | 7 ++ ocr/views.py | 204 +++++++++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+) create mode 100644 ocr/views.py diff --git a/docs/api/ocr.md b/docs/api/ocr.md index 159d257..1f5557d 100644 --- a/docs/api/ocr.md +++ b/docs/api/ocr.md @@ -161,3 +161,4 @@ requests.post(url, files=files) ### `DELETE /ocr/project//` 删除识别结果 + diff --git a/nocode_backend/settings.py b/nocode_backend/settings.py index 2e62833..f5b6492 100644 --- a/nocode_backend/settings.py +++ b/nocode_backend/settings.py @@ -32,6 +32,7 @@ INSTALLED_APPS = [ 'userManagement.apps.UsermanagementConfig', + 'ocr.apps.OcrConfig', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/nocode_backend/urls.py b/nocode_backend/urls.py index dbea088..938c1ef 100644 --- a/nocode_backend/urls.py +++ b/nocode_backend/urls.py @@ -19,4 +19,5 @@ urlpatterns = [ path('admin/', admin.site.urls), path('userManagement/', include('userManagement.urls')), + path('ocr/', include('ocr.urls')), ] diff --git a/ocr/urls.py b/ocr/urls.py index e69de29..bab4d5d 100644 --- a/ocr/urls.py +++ b/ocr/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls import url +from . import views + +urlpatterns = [ + url(r'^project/$', views.generalPro) + url(r'^project/(\d+)/$', views.detailPro), + url(r'^project/(?P\d+)/(?P\d+)/$', views.result), \ No newline at end of file diff --git a/ocr/views.py b/ocr/views.py new file mode 100644 index 0000000..ad658b6 --- /dev/null +++ b/ocr/views.py @@ -0,0 +1,204 @@ +from django.shortcuts import render +from django.http import HttpResponse, QueryDict +import json +from django.views.decorators.csrf import csrf_exempt +from ocr.models.project import Project +from ocr.models.recognition_result import RecognitionResult +from django.utils import timezone + + +def generalPro(request): + if request.method == 'POST': + """create a project + + [route]: /ocr/project + + [method]: POST + + [Request Body]: + name string + comment string + + [Response Body]: + id integer + created_at datetime + """ + print('Post success') + concat = request.POST + postBody = request.postBody + print(concat) + print(postBody) + json_result = json.loads(postBody) + print(json_result) + + pname = json_result['name'] + pcomment = json_result['comment'] + pcreated_at = timezone.now() + newProject = Project(name=pname, comment=pcomment, created_at=pcreated_at) + newProject.save() + + return JsonResponse({'id' : newProject.id, 'created_at' : pcreated_at}) + + else if request.method == 'GET': + """get information of all projects + + [route]: /ocr/project + + [method]: GET + + [Response Body]: + projects array + project_num integer + """ + return JsonResponse({'projects':Project.objects.all(), 'project_num':Project.objects.count()}) + + else: + return HttpResponse('方法错误') + +def detailPro(request, p1): + if request.method == 'PUT': + """modify specific project + + [route]: /ocr/project/ + + [method]: PUT + + [Request Body]: + name string + comment string + """ + put = QueryDict(request.body) + pname = put.get('name') + pcomment = put.get('comment') + + p = Project.objects.get(id=p1) + p.name = pname + p.comment = pcomment + p.save() + + else if request.method == 'GET': + """get information of specific project + + [route]: /ocr/project/ + + [method]: GET + + [Response Body]: + name string + comment string + created_at datetime + result_num integer + results array + """ + p = Project.objects.get(id=p1) + name = p.name + comment = p.comment + created_at = p.created_at + result_num = p.recognitionResult_set.count() + results = p.recognitionResult_set.all() + data = {'name':name, 'comment':comment, 'created_at':created_at, 'result_num':result_num, 'results':results} + return JsonResponse(data) + + else if request.method == 'DELETE': + """delete specific project + + [route]: /ocr/project/ + + [method]: DELETE + """ + p = Project.objects.get(id=p1) + p.delete() + + else if request.method == 'POST': + """upload image + + [route]: /ocr/project/ + + [method]: GET + + [Request Body]: + name string (optional, recognition result) + comment string (optional, recognition result) + + [Response Body]: + id integer + created_at datetime + """ + json_text = request.FILES.get("json", None) + json_result = json.loads(json_text) + print(json_result) + rname = json_result['name'] + rcomment = json_result['comment'] + + image = request.FILES.get("file", None) # png + if not image: + returnHttpResponse("no images for upload!") + + p = Project.objects.get(id=p1) + r = p.recognitionResult_set.create(name=rname, comment=rcomment, created_at=timezone.now()) + result = handle_ocr() + r.result = result + r.save() + + data = {"id":r.id, "created_at":r.created_at} + return JsonResponse(data) + + else: + return HttpResponse('方法错误') + +def result(request, p1, p2): + if request.method == 'GET': + """return recognition result + + [route]: /ocr/project// + + [method]: GET + + [Response Body]: + name string + comment string + result json + """ + p = Project.objects.get(id=p1) + r = p.recognitionResult_set.filter(id=p2) + data = {"name":r.name, "comment":r.comment, "result":r.result} + return JsonResponse(data) + + else if request.method == 'PUT': + """modify recognition result + + [route]: /ocr/project// + + [method]: PUT + + [Request Body]: + name string + comment string + """ + p = Project.objects.get(id=p1) + r = p.recognitionResult_set.filter(id=p2) + put = QueryDict(request.body) + rname = put.get('name') + rcomment = put.get('comment') + r.name = rname + r.comment = rcomment + + else if request.method == 'DELETE': + """delete recognition result + + [route]: /ocr/project// + + [method]: DELETE + """ + p = Project.objects.get(id=p1) + r = p.recognitionResult_set.filter(id=p2) + r.delete() + + else: + return HttpResponse('方法错误') + + + + +def handle_ocr(): + # ocr对接 \ No newline at end of file