From 7db1277dd9482744b541b1e0b327bb15f82c42b0 Mon Sep 17 00:00:00 2001 From: zy7y <13271962515@163.com> Date: Mon, 12 Sep 2022 15:11:12 +0800 Subject: [PATCH] feat: user api --- backend/.gitignore | 4 +- backend/controller/common.py | 12 +-- backend/controller/menu.py | 30 +----- backend/controller/role.py | 53 ++------- backend/controller/user.py | 42 ++++---- backend/core/__init__.py | 125 ++++++++++++++++++++++ backend/core/enums.py | 35 ------ backend/core/events.py | 4 +- backend/core/resp.py | 32 ------ backend/core/router.py | 48 --------- backend/core/security.py | 14 +-- backend/core/table.py | 4 +- backend/core/utils.py | 25 ----- backend/dbhelper/menu.py | 7 ++ backend/dbhelper/role.py | 8 ++ backend/dbhelper/user.py | 19 ++-- backend/main.py | 46 +++----- backend/mini.db | Bin 45056 -> 4096 bytes backend/mini.db-shm | Bin 32768 -> 32768 bytes backend/mini.db-wal | Bin 0 -> 226632 bytes backend/models/menu.py | 3 +- backend/models/user.py | 2 - backend/requirements.txt | 14 ++- backend/router/__init__.py | 0 backend/router/url.py | 23 ++++ backend/schemas/menu.py | 26 +++++ backend/schemas/role.py | 18 +++- backend/schemas/user.py | 22 +++- backend/tests/__init__.py | 0 backend/tests/test_case.py | 201 +++++++++++++++++++++++++++++++++++ 30 files changed, 513 insertions(+), 304 deletions(-) delete mode 100644 backend/core/enums.py delete mode 100644 backend/core/resp.py delete mode 100644 backend/core/router.py create mode 100644 backend/dbhelper/menu.py create mode 100644 backend/router/__init__.py create mode 100644 backend/router/url.py create mode 100644 backend/tests/__init__.py create mode 100644 backend/tests/test_case.py diff --git a/backend/.gitignore b/backend/.gitignore index f05c89f..adfd4f8 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -139,4 +139,6 @@ dmypy.json cython_debug/ .idea .pdm.toml -__pypackages__/ \ No newline at end of file +__pypackages__/ +.pytest_cache +.db-* \ No newline at end of file diff --git a/backend/controller/common.py b/backend/controller/common.py index 0831659..b40b2ac 100644 --- a/backend/controller/common.py +++ b/backend/controller/common.py @@ -1,16 +1,16 @@ -from core.resp import Response -from core.router import Router +from core import Response from core.security import generate_token, verify_password from dbhelper.user import get_user from schemas.common import LoginForm, LoginResult -common = Router(tags=["公共接口"]) - -@common.post("/login", summary="登录") async def login(auth_data: LoginForm) -> Response[LoginResult]: user_obj = await get_user({"username": auth_data.username}) if user_obj: if verify_password(auth_data.password, user_obj.password): - return Response(data=LoginResult(id=user_obj.id, token=generate_token(auth_data.username))) + return Response( + data=LoginResult( + id=user_obj.id, token=generate_token(auth_data.username) + ) + ) return Response(msg="账号或密码错误") diff --git a/backend/controller/menu.py b/backend/controller/menu.py index 6de874b..16ba0cb 100644 --- a/backend/controller/menu.py +++ b/backend/controller/menu.py @@ -1,31 +1,7 @@ -from core.resp import Response -from core.router import Router -from schemas.common import QueryData +from core import Response +from dbhelper.menu import insert_menu from schemas.menu import MenuIn, MenuRead -menu = Router(prefix="/menu", tags=["菜单管理"]) - -@menu.post("", summary="菜单添加") async def menu_add(data: MenuIn) -> Response[MenuRead]: - pass - - -@menu.get("/{pk}", summary="菜单详情") -async def menu_info(pk: int) -> Response[MenuRead]: - pass - - -@menu.delete("/{pk}", summary="删除菜单") -async def menu_del(pk: int) -> Response: - pass - - -@menu.put("/{pk}", summary="编辑菜单") -async def menu_put(pk: int, data: MenuIn) -> Response[MenuRead]: - pass - - -@menu.post("/list", summary="查询菜单列表") -async def menu_list(data: QueryData) -> Response[list[MenuRead]]: - pass + return Response(data=await insert_menu(data)) diff --git a/backend/controller/role.py b/backend/controller/role.py index 64be397..d918d9f 100644 --- a/backend/controller/role.py +++ b/backend/controller/role.py @@ -1,53 +1,20 @@ import json -from core.resp import Response -from core.router import Router +from core import Response from core.utils import list_to_tree -from dbhelper.role import get_role_menus -from schemas.common import QueryData -from schemas.role import RoleAdd, RoleInfo - -role = Router(prefix="/role", tags=["角色管理"]) +from dbhelper.role import get_role_menus, new_role +from schemas.role import RoleIn, RoleInfo -@role.post("", summary="角色添加") -async def role_add(data: RoleAdd) -> Response[RoleInfo]: - pass +async def role_add(data: RoleIn) -> Response[RoleInfo]: + return Response(data=await new_role(data)) -@role.get("/{pk}", summary="角色详情") -async def role_info(pk: int) -> Response[RoleInfo]: - pass - - -@role.delete("/{pk}", summary="删除角色") -async def role_del(pk: int) -> Response: - pass - - -@role.put("/{pk}", summary="编辑角色") -async def role_put(pk: int, data: RoleAdd) -> Response[RoleInfo]: - pass - - -@role.post("/list", summary="查询角色列表") -async def role_list(data: QueryData) -> Response[list[RoleInfo]]: - pass - - -@role.get("/{pk}/menu", summary="查询角色菜单权限") -async def role_menu(pk: int): - menus = await get_role_menus(pk) +async def role_has_menu(rid: int): + """ + rid: 角色ID + """ + menus = await get_role_menus(rid) for obj in menus: obj["meta"] = json.loads(obj["meta"]) if obj["meta"] is not None else None return Response(data=list_to_tree(menus)) - - -@role.get("/{pk}/menuIds", summary="查询角色菜单ids") -async def role_menus_id(): - pass - - -@role.get("/assign", summary="分配权限") -async def role_assign(): - pass diff --git a/backend/controller/user.py b/backend/controller/user.py index dada934..9ce6cae 100644 --- a/backend/controller/user.py +++ b/backend/controller/user.py @@ -1,22 +1,29 @@ -from core.resp import Response -from core.router import Router +from fastapi import Query + +from core import Response from core.security import get_password_hash -from dbhelper.user import get_user_info, get_users, insert_user +from dbhelper.user import get_user, get_user_info, get_users, insert_user, new_user from schemas.common import ListAll -from schemas.user import UserAdd, UserInfo, UserList, UserQuery - -user = Router(prefix="/user", tags=["用户管理"]) +from schemas.user import UserAdd, UserIn, UserInfo, UserList, UserQuery, UserRead -@user.post("", summary="用户添加") async def user_add(data: UserAdd) -> Response[UserInfo]: + """新增用户并分配角色 一步到位""" roles = data.rids del data.rids - user.password = get_password_hash(user.password) + data.password = get_password_hash(data.password) return await insert_user(data, roles) -@user.get("/{pk}", summary="用户详情") +async def create_user(data: UserIn) -> Response[UserRead]: + """新增用户""" + result = await get_user({"username": data.username}) + if result is None: + data.password = get_password_hash(data.password) + return Response(data=await new_user(data)) + return Response(msg="用户名已存在") + + async def user_info(pk: int) -> Response[UserInfo]: try: return Response(data=await get_user_info(pk)) @@ -24,18 +31,17 @@ async def user_info(pk: int) -> Response[UserInfo]: return Response(msg=f"用户不存在 {e}") -@user.delete("/{pk}", summary="删除用户") -async def user_del(pk: int) -> Response: - pass +async def user_arr( + offset: int = Query(default=1, description="偏移量-页码"), + limit: int = Query(default=10, description="数据量"), +) -> Response[ListAll[UserList]]: + skip = (offset - 1) * limit + users, count = await get_users(skip, limit) + return Response(data=ListAll(total=count, items=users)) -@user.put("/{pk}", summary="编辑用户") -async def user_put(pk: int, data: UserAdd) -> Response[UserInfo]: - pass - - -@user.post("/list", summary="查询用户列表") async def user_list(query: UserQuery) -> Response[ListAll[UserList]]: + """post查询用户列表""" limit = query.size skip = (query.offset - 1) * limit del query.offset, query.size diff --git a/backend/core/__init__.py b/backend/core/__init__.py index e69de29..3d14a10 100644 --- a/backend/core/__init__.py +++ b/backend/core/__init__.py @@ -0,0 +1,125 @@ +from typing import Generic, Optional, TypeVar + +from pydantic import BaseModel, Field +from pydantic.generics import GenericModel + +T = TypeVar("T") + + +class Response(GenericModel, Generic[T]): + code: int = 200 + data: Optional[T] + msg: str = "请求成功" + + +from datetime import datetime + + +class ReadBase(BaseModel): + """数据读取的基类""" + + id: int + status: int = Field(default=1, description="数据状态 1正常默认值 9 删除 5使用中 ") + created: datetime + modified: datetime + + +from typing import Any, Callable, get_type_hints + +from fastapi import routing + + +class Route(routing.APIRoute): + """ + https://github.com/tiangolo/fastapi/issues/620 + Django挂载视图方法 + def index() -> User: + pass + Route("/", endpoint=index) + """ + + def __init__( + self, + path: str, + endpoint: Callable[..., Any], + tags: list[str], + summary: str, + **kwargs: Any + ): + if kwargs.get("response_model") is None: + kwargs["response_model"] = get_type_hints(endpoint).get("return") + super(Route, self).__init__( + path=path, endpoint=endpoint, tags=tags, summary=summary, **kwargs + ) + + @classmethod + def post( + cls, + path: str, + endpoint: Callable[..., Any], + tags: list[str], + summary: str, + **kwargs: Any + ): + return Route( + path=path, + endpoint=endpoint, + methods=["POST"], + tags=tags, + summary=summary, + **kwargs + ) + + @classmethod + def get( + cls, + path: str, + endpoint: Callable[..., Any], + tags: list[str], + summary: str, + **kwargs: Any + ): + return Route( + path=path, + endpoint=endpoint, + methods=["GET"], + tags=tags, + summary=summary, + **kwargs + ) + + @classmethod + def delete( + cls, + path: str, + endpoint: Callable[..., Any], + tags: list[str], + summary: str, + **kwargs: Any + ): + return Route( + path=path, + endpoint=endpoint, + methods=["DELETE"], + tags=tags, + summary=summary, + **kwargs + ) + + @classmethod + def put( + cls, + path: str, + endpoint: Callable[..., Any], + tags: list[str], + summary: str, + **kwargs: Any + ): + return Route( + path=path, + endpoint=endpoint, + methods=["PUT"], + tags=tags, + summary=summary, + **kwargs + ) diff --git a/backend/core/enums.py b/backend/core/enums.py deleted file mode 100644 index 27114a3..0000000 --- a/backend/core/enums.py +++ /dev/null @@ -1,35 +0,0 @@ -import enum - - -class Status(enum.IntEnum): - """ - 数据库状态枚举值 - 9 删除 5 无效 1 有效 3 使用 - """ - - DELETED: int = 9 - INACTIVE: int = 5 - ACTIVE: int = 1 - SELECTED: int = 3 - - -class UserType(enum.IntEnum): - """ - 数据库超级管理员枚举 - 0 超级管理员 1用户 - """ - - ADMIN: int = 0 - USER: int = 1 - - -class MenuType(enum.IntEnum): - """ - 菜单类型枚举 - 目录 0 - 组件 1 按钮 2 - """ - - DIR = 0 - CPN = 1 - BTN = 2 diff --git a/backend/core/events.py b/backend/core/events.py index ef1194a..26d4feb 100644 --- a/backend/core/events.py +++ b/backend/core/events.py @@ -3,9 +3,7 @@ from tortoise import Tortoise async def init_orm(): """初始化orm""" - await Tortoise.init( - db_url="sqlite://mini.db", modules={"models": ["models"]}, - ) + await Tortoise.init(db_url="sqlite://mini.db", modules={"models": ["models"]}) await Tortoise.generate_schemas() diff --git a/backend/core/resp.py b/backend/core/resp.py deleted file mode 100644 index bf79afd..0000000 --- a/backend/core/resp.py +++ /dev/null @@ -1,32 +0,0 @@ -import enum -from typing import Generic, Optional, TypeVar, Union - -from pydantic.generics import GenericModel - -T = TypeVar("T") - - -class Status(enum.IntEnum): - OK = 200 - CREATED = 201 - ACCEPTED = 202 - NO_CONTENT = 204 - BAD_REQUEST = 400 - UNAUTHORIZED = 401 - FORBIDDEN = 403 - NOT_FOUND = 404 - INTERNAL_SERVER_ERROR = 500 - NOT_IMPLEMENTED = 501 - BAD_GATEWAY = 502 - SERVICE_UNAVAILABLE = 503 - - -class Msg(enum.Enum): - OK = "OK" - FAIL = "FAIL" - - -class Response(GenericModel, Generic[T]): - code: Status = Status.OK - data: Optional[T] - msg: Union[Msg, str] = Msg.OK diff --git a/backend/core/router.py b/backend/core/router.py deleted file mode 100644 index b04d253..0000000 --- a/backend/core/router.py +++ /dev/null @@ -1,48 +0,0 @@ -from typing import TYPE_CHECKING, Any, Callable, get_type_hints - -from fastapi import APIRouter -from fastapi.routing import APIRoute - -""" -根据类型标注自动返回模型 -https://github.com/tiangolo/fastapi/issues/620 -""" - - -class Router(APIRouter): - """ - 装饰器用法 - @app.get("/") - def index() -> User: - """ - - if not TYPE_CHECKING: - - def add_api_route( - self, path: str, endpoint: Callable[..., Any], **kwargs: Any - ) -> None: - if kwargs.get("response_model") is None: - kwargs["response_model"] = get_type_hints(endpoint).get("return") - return super().add_api_route(path, endpoint, **kwargs) - - else: # pragma: no cover - pass - - -class Route(APIRoute): - """ - Django挂载视图方法 - def index() -> User: - pass - Route("/", endpoint=index) - """ - - if not TYPE_CHECKING: - - def __init__(self, path: str, endpoint: Callable[..., Any], **kwargs: Any): - if kwargs.get("response_model") is None: - kwargs["response_model"] = get_type_hints(endpoint).get("return") - super(Route, self).__init__(path=path, endpoint=endpoint, **kwargs) - - else: # pragma: no cover - pass diff --git a/backend/core/security.py b/backend/core/security.py index 793c7aa..c56d9f2 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -14,7 +14,7 @@ SECRET_KEY = "lLNiBWPGiEmCLLR9kRGidgLY7Ac1rpSWwfGzTJpTmCU" ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_MINUTES = 60 *24 * 7 +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") bearer = HTTPBearer() @@ -45,13 +45,9 @@ def generate_token(username: str, expires_delta: Optional[timedelta] = None): if expires_delta: expire = datetime.utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta( - minutes=ACCESS_TOKEN_EXPIRE_MINUTES - ) + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update(dict(exp=expire)) - encoded_jwt = jwt.encode( - to_encode, SECRET_KEY, algorithm=ALGORITHM - ) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt @@ -59,9 +55,7 @@ async def check_token(security: HTTPAuthorizationCredentials = Depends(bearer)): """检查用户token""" token = security.credentials try: - payload = jwt.decode( - token, SECRET_KEY, algorithms=[ALGORITHM] - ) + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") return await get_user({"username": username}) except JWTError: diff --git a/backend/core/table.py b/backend/core/table.py index d9be1f4..24fa5cd 100644 --- a/backend/core/table.py +++ b/backend/core/table.py @@ -1,7 +1,5 @@ from tortoise import fields, models -from core.enums import Status - class Table(models.Model): """ @@ -9,7 +7,7 @@ class Table(models.Model): """ id = fields.IntField(pk=True, description="主键") - status = fields.IntEnumField(Status, description="状态", default=Status.ACTIVE) + status = fields.SmallIntField(default=1, description="状态 1有效 9 删除 5选中") created = fields.DatetimeField(auto_now_add=True, description="创建时间", null=True) modified = fields.DatetimeField(auto_now=True, description="更新时间", null=True) diff --git a/backend/core/utils.py b/backend/core/utils.py index 3385fc1..697c282 100644 --- a/backend/core/utils.py +++ b/backend/core/utils.py @@ -24,28 +24,3 @@ def list_to_tree( else: arr.append(menu) return arr - - -def menu_table(): - """生成菜单表数据""" - from models import MenuModel - MenuModel.bulk_create([ - MenuModel(name="系统管理", - meta={"icon": "Grid"}, - path="/system", - type=0), - MenuModel(name="系统设置", - meta={"icon": "Setting"}, - path="/setting", - type=0), - MenuModel(name="菜单管理", - meta={"icon": "Menu"}, - path="/system/menu", - type=1, - component="/system/menu", - pid=1, - api="/menu", - method="{'GET}", - regx="^/menu$" - ) - ]) \ No newline at end of file diff --git a/backend/dbhelper/menu.py b/backend/dbhelper/menu.py new file mode 100644 index 0000000..dd90c43 --- /dev/null +++ b/backend/dbhelper/menu.py @@ -0,0 +1,7 @@ +from models import MenuModel +from schemas.menu import MenuIn + + +async def insert_menu(menu: MenuIn): + """新增菜单""" + return await MenuModel.create(**menu.dict()) diff --git a/backend/dbhelper/role.py b/backend/dbhelper/role.py index c7ab763..9e2d8a0 100644 --- a/backend/dbhelper/role.py +++ b/backend/dbhelper/role.py @@ -1,5 +1,8 @@ from tortoise import connections +from models import RoleModel +from schemas.role import RoleIn + async def get_role_menus(rid: int): """ @@ -13,3 +16,8 @@ async def get_role_menus(rid: int): AND sys_role_menu.rid = (%s) AND m.`status` = 1 ORDER BY m.sort""", [rid], ) + + +async def new_role(role: RoleIn): + """新增角色""" + return await RoleModel.create(**role.dict()) diff --git a/backend/dbhelper/user.py b/backend/dbhelper/user.py index 72f307d..b397a24 100644 --- a/backend/dbhelper/user.py +++ b/backend/dbhelper/user.py @@ -1,8 +1,7 @@ from tortoise.transactions import atomic -from core.enums import Status from models import RoleModel, UserModel, UserRoleModel -from schemas.user import UserRole +from schemas.user import UserIn, UserRole async def get_user(kwargs): @@ -32,13 +31,13 @@ async def get_user_info(pk: int): active_rid = role[0].get("rid") rids = [] for obj in role: - if obj.get("status") == Status.SELECTED: + if obj.get("status") == 5: active_rid = obj.get("rid") rids.append(obj.get("rid")) return {**user, "active_rid": active_rid, "rids": rids} -async def get_users(skip: int, limit: int, kwargs: dict): +async def get_users(skip: int, limit: int, kwargs: dict = None): """ 分页获取用户并且支持字段模糊查询 Args: @@ -49,7 +48,10 @@ async def get_users(skip: int, limit: int, kwargs: dict): Returns: """ - kwargs = {f"{k}__contains": v for k, v in kwargs.items()} + if kwargs is not None: + kwargs = {f"{k}__contains": v for k, v in kwargs.items()} + else: + kwargs = {} result = ( UserModel.filter(status__not_in=[9, 5], **kwargs).all().order_by("-created") ) @@ -66,7 +68,12 @@ async def insert_user(user, roles): user_role = UserRole(rid=rid, uid=obj.id) if index == 0: - user_role.status = Status.SELECTED + user_role.status = 5 # 第一个角色默认, 添加到关系表 await UserRoleModel.create(**user_role.dict()) return user + + +async def new_user(user: UserIn): + """新增用户""" + return await UserModel.create(**user.dict()) diff --git a/backend/main.py b/backend/main.py index b9c525e..21ad460 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,41 +1,23 @@ from fastapi import FastAPI -from fastapi.openapi.docs import get_swagger_ui_html -from controller import register_routers from core.events import close_orm, init_orm -from core.log import logger_db_client -from core.utils import menu_table +from core.log import logger_db_client as logger +from core.middleware import middlewares +from router.url import routes app = FastAPI( - on_startup=[init_orm, menu_table], + on_startup=[init_orm], on_shutdown=[close_orm], - docs_url=None, - redoc_url=None, + routes=routes, + middleware=middlewares, ) -register_routers(app) - - -@app.get("/docs", include_in_schema=False) -async def custom_swagger_ui_html(): - return get_swagger_ui_html( - openapi_url=app.openapi_url, - title=app.title + " - Swagger UI", - oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url, - swagger_js_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/4.10.3/swagger-ui-bundle.js", - swagger_css_url="https://cdn.bootcdn.net/ajax/libs/swagger-ui/4.10.3/swagger-ui.css", - ) - - -for i in app.routes: - logger_db_client.debug(i.__dict__) - logger_db_client.info(f"{i.path}, {i.methods}, {i.path_regex}") - """ - 'path_regex': re.compile('^/role/(?P< -pk>[^/]+)/menu$'), 'path_format': '/role/{pk}/menu', - """ - - -if __name__ == '__main__': +if __name__ == "__main__": import uvicorn - uvicorn.run("main:app", reload=True) \ No newline at end of file + + for i in app.routes: + logger.info( + f"{i.path}, {i.methods}, {i.path_regex}, {i.__dict__.get('summary')}, {i.endpoint}" + ) + + uvicorn.run("main:app", reload=True) diff --git a/backend/mini.db b/backend/mini.db index 1975609e80f9898e768ddaaacd2002cc6b7e989e..e2a2455a104f1cf6a5d6b143f3ac8831aaaf3ab5 100644 GIT binary patch delta 26 gcmZp8z|^2HL7J73fq{W>Vxv8fq32(_Nnj2?09pD5a{vGU literal 45056 zcmeI)>rYc>90%}TxK)sT$jtTWMifzwhbkZf6jiqH$#3jyCWhxQ;9dRlrJ?UF@1 zl(9L7ab9B7MPoL7Gr`5-CX2q_AF(%v9%{1e#olZ$_dKVU)0=V2d|}Ax^#dxHCfJs9VinQ%13#hEU}UWH1f_>f@~3WbV1c9BP6Ya%b|3UA1>vgGxvUaAxw zW1sA9`aq#k|Eg%&YPsCfu1Pij-E^$}kEY#qqhu#G2teTf3!Kq3YApwp>Gy&{fKLVk zNnb1#^Ce<@)E5o=62XAa%=Z|0L&@HGe%GMOYVlZ^5$BL~lF_|7myQ{8GP<(7I_98` z6stSJ=%VCFzw~Z%qxQf7<+3u)P0#aXY0KVd@|Ail19oe5p&Ev4q$xVmDvx#8>SD%S zBMyseim_Rz7>n04HsT~DIIK<&;~XOouiegc9%8nB+I)HCA#zm= z*zKe!IWaS2J!bLRJ&b|o+x}uhJe!vD`J+4+=SlUR5r@_7u{g#_dkQ%Qy)GB2*e4$& zC1h4^W;UOS*Ee2Xe=L_13I~ES!T(f{c=NG%ZN0J}s!J_OPJmRJy?;6T`47Q>Tp{g@ z>U7&z9*8SXq;wknxpx{Hwe9W7m0e|hROp_j+C4*h8l6(nyY#{mxDc;nCM>SO;}+LJ zLyunWA+K}fgx5;LEBpAn%<2=lmxQ2yt~{fW)sqOLadiLd%%zKR(g+ueErz3{SBrHR zdrK;j@~kay-;twtaLnoUxGXeU=*20I0r~d&jD3D?n$t1)29is-a3Xq*zO$)8JJ_yN zNJB_$VViSi*XIwyyTktHEjBcHgsnte|5Y4m66YfTrlS9~GzLqf-I zHqZ2!>ClkhD7tmntkU}0!9$uUBoIN>5%h1T z3)&gg(?xdmR_5Z>ks&EaV(Lci`|ZkfTiKd|!u#T>2pJn#1IJI(-lF^JYDY(9ayd5g zJ*cB)(YTOeUq7yD&>lXloXU+Pxj`t#FC=)u&*#Ua!drD&XjB|LNQ*l{lIoY+l+x(* zWqomUqE#2#ttu-;TIA$ghFPy-)~gbiLG;nwrJ zVtP#;xFTG9Rvxwtbas<64u z)P62dvNq!)K{=Crp5j;EW>)W2wu11@hD&GtVkJ`Y?AjCY(Wm0IpEJL#%1w{*r;>6N z-Q`(wHHhEe%dFhU-hGgL_AHlIqu))Z@4fUFHV8lf0uX=z1Rwwb2tWV=5P$##cB%mV z`(N4mr-J-og8&2|009U<00Izz00bZa0SG`~#|X42)$K}p?+d+4L9L?{F%|G%^P z6K4Yf2tWV=5P$##AOHafKmY;|s3m~!|7)pW9|Rx(0SG_<0uX=z1Rwwb2tZ(G3DCd) z)q6%2tqXg8BTv{M009U<00Izz00bZa0SG_<0pTe{xJ8jVNUzM}>M zV=(ozrhXIK(bLPC`??OZY(LAs?szN_;iK7&^IK1!?l(^FHyHODq9)eX8w(8bld0+C zz-jkz%0J$bJlQioWaE27Trw_nx?QJSj;@a*#+2=J++wkF>3u4xNo}>7#F{#cW>c5h z_=+Y~9FOy{I3E>%ypUbHnr~C@lxNO9K9@M=buZdOrWyaq&Pa?Gy4iVWk9Uw4LIKx}n3;_s000Izz00bZa0SG_<0uX?}TPo13)+(jXQ`8!Dn=G_Y{3HT!U_y+g^7W(3}XWuKv@7YBTlQ1Qj&>* z$vyv5)%SGu?Y@1_ZviKHYAHnBMnx;YRo7?x=edQ|o z<9_?pSG4W<@gw5$ce1;cwUM(C=6x~eV=ly8jJe DxjYmQ diff --git a/backend/mini.db-wal b/backend/mini.db-wal index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9f19091e79e94ff42d6f3a7e57592838915268fb 100644 GIT binary patch literal 226632 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