Global API refactoring

This commit is contained in:
2022-10-09 13:43:06 +03:00
parent ad659b5f30
commit 47359a7932
11 changed files with 471 additions and 213 deletions

View File

@@ -5,4 +5,9 @@ from .models import *
@admin.register(UserToken)
class DevEventAdmin(admin.ModelAdmin):
readonly_fields = ['access_token']
list_display = ['user', 'creation_time', 'small_access_token']
fields = ['user', 'creation_time', 'access_token']
ordering = ['-creation_time']
def small_access_token(self, obj):
return f"{obj.access_token[:8]}..."

View File

@@ -7,6 +7,8 @@ from arka.settings import PHONE_VERIFICATION_RESEND_TIME_SECS
API_OK_OBJ = {"status": "success"}
API_ERROR_MULTIPLY_ERRORS = (None, 'multiply errors')
API_ERROR_INTERNAL_ERROR = (100, 'internal error')
API_ERROR_METHOD_NOT_FOUND = (200, 'method not found')
@@ -43,29 +45,40 @@ API_ERROR_VALIDATION = {
}
def make_error_object(ex: Exception):
data = {
"status": "error"
def __make_error(ex: Exception):
if type(ex.args[0]) != tuple:
raise ex
error = {
"code": ex.args[0][0],
"message": ex.args[0][1]
}
if len(ex.args) >= 2:
error["related"] = ex.args[1]
return error
def make_error_object(ex: Exception | list):
try:
if type(ex.args[0]) != tuple:
raise ex
data["error"] = {
"code": ex.args[0][0],
"message": ex.args[0][1]
data = {
"status": "error"
}
if len(ex.args) >= 2:
data["error"]["related"] = ex.args[1]
if type(ex) == list:
data["error"] = [__make_error(e) for e in ex]
else:
data["error"] = [__make_error(ex)]
return data
except BaseException as err:
traceback.print_exc()
data["error"] = {
"code": API_ERROR_INTERNAL_ERROR[0],
"message": API_ERROR_INTERNAL_ERROR[1],
"related": f"Exception {type(err)}: {str(err)}"
return {
"status": "error",
"error": [{
"code": API_ERROR_INTERNAL_ERROR[0],
"message": API_ERROR_INTERNAL_ERROR[1],
"related": f"Exception {type(err)}: {str(err)}"
}]
}
return data

View File

@@ -1,6 +1,6 @@
from .api_utils import *
from .models import *
from django.core.exceptions import *
from order.models import *
def _require_access_token(params):
@@ -8,169 +8,239 @@ def _require_access_token(params):
return UserToken.get_user_by_token(token)
def account_auth(params):
login = api_get_param_str(params, "login")
password = api_get_param_str(params, "password")
user = UserToken.auth(login, password)
token = UserToken.create_token(user)
class ApiAccount:
@staticmethod
@api_method("account.auth",
doc="Аутентификация пользователя",
params=[
api_make_param("login", str, "Логин пользователя"),
api_make_param("password", str, "Пароль пользователя"),
],
returns="В случае правильных логина и пароля <code>access_token</code>. "
"В противном случае объект ошибки.")
def auth(login, password):
user = UserToken.auth(login, password)
token = UserToken.create_token(user)
return api_make_response({"access_token": token.access_token})
return api_make_response({"access_token": token.access_token})
@staticmethod
@api_method("account.deauth",
doc="Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена",
params=[
API_PARAM_ACCESS_TOKEN,
], returns="В случае успеха стандартный код успеха")
def deauth(access_token):
UserToken.deauth(access_token.access_token)
return api_make_response({})
def account_deauth(params):
UserToken.deauth(api_get_param_str(params, "access_token"))
return api_make_response({})
@staticmethod
@api_method("account.register",
doc="Регистрация нового пользователя",
params=[
api_make_param("name", str, "Имя пользователя"),
api_make_param("surname", str, "Фамилия пользователя"),
api_make_param("phone", str, "Телефон в формате <code>[[+]7]1112223333</code> "
"<i>(в квадратных скобках необязательная часть)</i>"),
api_make_param("email", str, "Почта"),
api_make_param("password", str, "Пароль пользователя"),
], returns="Аналогично методу <code>account.auth</code> в случае успеха")
def register(name, surname, phone, email, password):
def account_register(params):
name = api_get_param_str(params, "name")
surname = api_get_param_str(params, "surname")
phone = api_get_param_str(params, "phone")
email = api_get_param_str(params, "email")
password = api_get_param_str(params, "password")
user = SiteUser(
name=name,
surname=surname,
phone=phone,
email=email,
password=password
)
try:
user.full_clean()
user.save()
user = SiteUser.create_user(
name=name,
surname=surname,
phone=phone,
email=email,
password=password
)
try:
token = UserToken.create_token(user)
return api_make_response({"access_token": token.access_token})
except Exception as ex:
# если вдруг токен нельзя создать
user.delete()
raise ex
except ValidationError as validation_error:
traceback.print_exc()
errors = {}
for field_name in validation_error.error_dict:
err_list = validation_error.error_dict[field_name]
print(err_list)
obj = []
for err in err_list:
obj.append({
"code": err.code
})
errors[field_name] = obj
raise Exception(API_ERROR_USER_REGISTER, errors)
def account_verify_phone(params):
user = _require_access_token(params)
if user.is_phone_verified:
raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED)
code = api_get_param_int(params, "code", False, None)
if code is None:
res, err_code = PhoneVerificationService.send_verify(user.phone)
if not res:
if err_code in API_ERROR_VALIDATION:
raise Exception(API_ERROR_VALIDATION[err_code])
else:
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
return api_make_response({"action": "phone_call"})
else:
res, err_code = PhoneVerificationService.check_code(user.phone, code)
if res:
user.is_phone_verified = True
user.full_clean()
user.save()
return api_make_response({})
try:
token = UserToken.create_token(user)
return api_make_response({"access_token": token.access_token})
except Exception as ex:
# если вдруг токен нельзя создать
user.delete()
raise ex
except ValidationError as validation_error:
traceback.print_exc()
errors = {}
for field_name in validation_error.error_dict:
err_list = validation_error.error_dict[field_name]
print(err_list)
obj = []
for err in err_list:
obj.append({
"code": err.code
})
errors[field_name] = obj
raise Exception(API_ERROR_USER_REGISTER, errors)
@staticmethod
@api_method("account.verifyPhone",
doc="Запросить верификацию номера телефона."
"Если телефон уже верифицирован, метод вернет соответствующую ошибку",
params=[
API_PARAM_ACCESS_TOKEN,
api_make_param("code", int, "Код верификации. Если не передать, будет выполнен звонок", False),
],
returns='{"status": "success"}, если верификация пройдена. Иначе одну из стандартных ошибок')
def verify_phone(access_token, code):
user = access_token.user
if user.is_phone_verified:
raise Exception(API_ERROR_VALIDATION_CURRENTLY_VERIFIED)
if code is None:
res, err_code = PhoneVerificationService.send_verify(user.phone)
if not res:
if err_code in API_ERROR_VALIDATION:
raise Exception(API_ERROR_VALIDATION[err_code])
else:
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
return api_make_response({"action": "phone_call"})
else:
if err_code in API_ERROR_VALIDATION:
raise Exception(API_ERROR_VALIDATION[err_code])
res, err_code = PhoneVerificationService.check_code(user.phone, code)
if res:
user.is_phone_verified = True
user.save()
return api_make_response({})
else:
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
if err_code in API_ERROR_VALIDATION:
raise Exception(API_ERROR_VALIDATION[err_code])
else:
raise Exception(API_ERROR_VALIDATION_UNKNOWN)
@staticmethod
@api_method("account.get",
doc="Получение информации о пользователе",
params=[
API_PARAM_ACCESS_TOKEN,
],
returns="Поля пользователя (name, surname, email, phone, phone_verified).")
def get(access_token):
user = access_token.user
return api_make_response({
"id": user.id,
"name": user.name,
"surname": user.surname,
"email": user.email,
"phone": user.phone,
"phone_verified": user.is_phone_verified
})
def account_get(params):
user = _require_access_token(params)
return api_make_response({
"id": user.id,
"name": user.name,
"surname": user.surname,
"email": user.email,
"phone": user.phone,
"phone_verified": user.is_phone_verified
})
class ApiOrder:
@staticmethod
@api_method("order.getForm",
doc="Получение формы создания заказа в виде полей, которые нужно показать пользователю",
params=[],
returns="<code>JSON</code> объект, содержащий данные формы. Структура пока не определена")
def get_form():
return api_make_response({
"fields": [
# name = models.CharField(max_length=200, verbose_name="Название заказа")
{
"name": "name",
"label": "Название заказа",
"widget": "text",
"attrs": {
"max_len": 200
},
"required": True
},
# square = models.DecimalField(max_digits=7, decimal_places=2, blank=False, verbose_name="Площадь в м²")
{
"name": "square",
"label": "Площадь в м²",
"widget": "decimal",
"attrs": {
"max_digits": 7,
"decimal_places": 2
},
"required": True
},
# work_time = models.CharField(max_length=100, blank=True, verbose_name="Рабочее время")
{
"name": "work_time",
"label": "Рабочее время",
"widget": "text",
"attrs": {
"max_len": 100
},
"required": False
},
# type_of_renovation = models.CharField(max_length=10, choices=TYPE_OF_RENOVATION_CHOICES,
# default=CHOICE_UNDEFINED, blank=True, verbose_name="Тип ремонта")
{
"name": "type_of_renovation",
"label": "Тип ремонта",
"widget": "choice",
"attrs": {
"default": None,
"choices": Order.TYPE_OF_RENOVATION_CHOICES
},
"required": False
},
# type_of_room = models.CharField(max_length=10, choices=TYPE_OF_ROOM_CHOICES,
# blank=True, default=CHOICE_UNDEFINED, verbose_name="Тип квартиры")
{
"name": "type_of_room",
"label": "Тип квартиры",
"widget": "radio",
"attrs": {
"default": None,
"choices": Order.TYPE_OF_ROOM_CHOICES
},
"required": False
},
# флажок
{
"name": "is_with_warranty",
"label": "С гарантией",
"widget": "checkbox",
"attrs": {
"default": True
},
"required": False
},
]
})
def __make_argument_doc(name, arg_type, description, required=True):
return {
"name": name,
"type": arg_type,
"description": description,
"required": required
}
def api_call_method(method_name, params: dict):
try:
if method_name in api_methods_dict:
out = api_methods_dict[method_name]["func"](**params)
if out is None:
raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object")
else:
raise Exception(API_ERROR_METHOD_NOT_FOUND)
except Exception as ex:
traceback.print_exc()
out = make_error_object(ex)
return out
def __make_argument_access_token():
return __make_argument_doc("access_token", "string", "<i>Токен</i>, выданный методом <code>account.auth</code>")
__doc_type_string = "string"
api_methods = {
"account.auth": {
"func": account_auth,
"doc": "Аутентификация пользователя",
"params": [
__make_argument_doc("login", __doc_type_string, "Логин пользователя"),
__make_argument_doc("password", __doc_type_string, "Пароль пользователя"),
],
"returns": "В случае правильных логина и пароля <code>access_token</code>. В противном случае объект ошибки."
},
"account.deauth": {
"func": account_deauth,
"doc": "Удаление токена, дальшейшие вызовы API с этим токеном вернут ошибку невалидного токена",
"params": [
__make_argument_access_token()
],
"returns": "В случае успеха стандартный код успеха"
},
"account.register": {
"func": account_register,
"doc": "Регистрация нового пользователя",
"params": [
],
"returns": "Поля пользователя (id, name, surname, email, phone, phone_verified)."
},
"account.verifyPhone": {
"func": account_verify_phone,
"doc": "Запросить верификацию номера телефона."
"Если телефон уже верифицирован, метод вернет соответствующую ошибку",
"params": [
__make_argument_access_token(),
__make_argument_doc("code", __doc_type_string, "Код верификации. Если не передать, будет выполнен звонок"),
],
"returns": '{"status": "success"}, если верификация пройдена. Иначе одну из стандартных ошибок'
},
"account.get": {
"func": account_get,
"doc": "Получение информации о пользователе",
"params": [
__make_argument_access_token()
],
"returns": "Поля пользователя (name, surname, email, phone, phone_verified)."
},
}
def api_get_documentation():
# {
# "name": p["name"],
# "type": p["type"],
# "description": p["description"],
# "required": p["required"]
# }
return []

View File

@@ -1,4 +1,6 @@
from .api_errors import *
from .models import UserToken
api_methods_dict = {}
def __make_invalid_argument_type_error(name, value, except_type):
@@ -10,7 +12,7 @@ def api_make_response(response):
return API_OK_OBJ | {"response": response}
def api_get_param_int(params: dict, name: str, required=True, default=0):
def api_get_param_int(params: dict, name: str, required=True, default=None):
if name in params:
try:
return int(params[name])
@@ -30,4 +32,70 @@ def api_get_param_str(params: dict, name: str, required=True, default=""):
if required:
raise Exception(API_ERROR_MISSING_ARGUMENT, name)
return default
return None
def api_get_access_token(params: dict, unused_name, required=True):
token = api_get_param_str(params, "access_token", required)[0]
print(f"checking token '{token}'")
return UserToken.get_by_token(token)
def api_make_param(name, arg_class, description, required=True):
return {
"name": name,
"type": arg_class,
"description": description,
"required": required
}
API_PARAM_ACCESS_TOKEN = api_make_param(
"access_token",
UserToken,
"<i>Токен</i>, выданный методом <code>account.auth</code>"
)
def api_method(func_name, doc="", params: list or None = None, returns=""):
"""
Декоратор для методов API, автоматически передает параметры методам
"""
def actual_decorator(func):
def wrapper(**kwargs):
print(f"> call method {func_name} with params {kwargs}. method params: {params}")
errors = []
func_args = {}
for p in params:
parser_funcs = {
str: api_get_param_str,
int: api_get_param_int,
UserToken: api_get_access_token
}
try:
if p["type"] not in parser_funcs:
raise Exception(API_ERROR_INTERNAL_ERROR, f"param type {p['type']} is unsupported")
func_args[p["name"]] = parser_funcs[p["type"]](kwargs, p["name"], p["required"])
except Exception as ex:
errors.append(ex)
print(f"errors: {errors}, args: {func_args}")
if len(errors) > 0:
return make_error_object(errors)
else:
out = func(**func_args)
if out is None:
raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object")
return out
api_methods_dict[func_name] = {
"doc": doc,
"params": params,
"func": wrapper,
"return": returns
}
return wrapper
return actual_decorator

View File

@@ -46,11 +46,15 @@ class UserToken(models.Model):
@staticmethod
def get_user_by_token(token: str):
t = UserToken.objects.filter(access_token=token)
return UserToken.get_by_token(token).user
@staticmethod
def get_by_token(token: str):
t = UserToken.objects.filter(access_token=token).select_related('user')
if len(t) == 0:
raise Exception(API_ERROR_INVALID_TOKEN)
return t[0].user
return t[0]
def __str__(self):
return self.user.email + ": " + self.access_token[:10] + "..."

View File

@@ -3,12 +3,23 @@ import traceback
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseBadRequest
from .api_methods import api_methods
from .api_methods import api_call_method, api_get_documentation
from .api_errors import *
def view_methods(request):
return render(request, 'api/index.html', {'api_methods': api_methods})
methods = []
def __make_param(p):
return {
"name": p["name"],
"type": p["type"],
"description": p["description"],
"required": p["required"]
}
methods = api_get_documentation()
return render(request, 'api/index.html', {'api_methods': methods})
def call_method(request, method_name):
@@ -19,17 +30,8 @@ def call_method(request, method_name):
else:
return HttpResponseBadRequest()
try:
if method_name in api_methods:
out = api_methods[method_name]["func"](params)
if out is None:
raise Exception(API_ERROR_INTERNAL_ERROR, "method returned null object")
else:
raise Exception(API_ERROR_METHOD_NOT_FOUND)
except Exception as ex:
traceback.print_exc()
out = make_error_object(ex)
out = api_call_method(method_name, params)
response = HttpResponse(json.dumps(out, ensure_ascii=False))
response.headers["Content-type"] = "application/json"
response.headers["Content-type"] = "application/json; charset=utf-8"
return response