From 2becd337746072f3c0106180debed84264f2d4ec Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Thu, 1 Aug 2024 11:16:08 +0300 Subject: [PATCH] wip --- src/ocab_core/ocab_core/main.py | 2 +- .../ocab_modules/standard/config/main.py | 1 + .../standard/config/miniapp_ui.py | 45 ++++++- .../standard/miniapp/dash_telegram_auth.py | 125 +++++++++++++++++- .../ocab_modules/standard/miniapp/lib.py | 121 +++++++++++------ 5 files changed, 241 insertions(+), 53 deletions(-) diff --git a/src/ocab_core/ocab_core/main.py b/src/ocab_core/ocab_core/main.py index 71f630c..83a0b5c 100644 --- a/src/ocab_core/ocab_core/main.py +++ b/src/ocab_core/ocab_core/main.py @@ -78,7 +78,7 @@ class OCAB: singleton = Singleton() app = FastAPI() config = get_module("standard.config", "config") - app.mount("/webapp", singleton.storage["webapp"]) + app.mount(config.get("miniapp::prefix"), singleton.storage["webapp"]) await register_bot_webhook(app, singleton.bot, singleton.dp) await singleton.bot.set_webhook(config.get("core::webhook::public_url")) hyperConfig = HyperConfig() diff --git a/src/ocab_modules/ocab_modules/standard/config/main.py b/src/ocab_modules/ocab_modules/standard/config/main.py index b482db8..89096a0 100644 --- a/src/ocab_modules/ocab_modules/standard/config/main.py +++ b/src/ocab_modules/ocab_modules/standard/config/main.py @@ -15,6 +15,7 @@ def register_settings_page(): path="/settings", blueprint=get_miniapp_blueprint(config, prefix), prefix=prefix, + role="ADMIN", ) pass diff --git a/src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py b/src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py index 53c44d6..cd6fabd 100644 --- a/src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py +++ b/src/ocab_modules/ocab_modules/standard/config/miniapp_ui.py @@ -1,13 +1,22 @@ +import asyncio +from typing import TYPE_CHECKING + from .config_manager import ConfigManager try: import dash_bootstrap_components as dbc + import flask from dash_extensions.enrich import ALL, Input, Output, State, dcc, html DASH_AVAILABLE = True except ImportError: DASH_AVAILABLE = False +from ocab_core.modules_system.public_api import get_module + +if TYPE_CHECKING: + from ocab_modules.standard.roles import Roles as IRoles + def create_control(key: str, config: ConfigManager): value = config.get(key) @@ -126,6 +135,10 @@ def create_settings_components(tree, level=0): def get_miniapp_blueprint(config: ConfigManager, prefix: str): + Roles: "type[IRoles]" = get_module("standard.roles", "Roles") + + roles = Roles() + import datetime from dash_extensions.enrich import DashBlueprint @@ -171,6 +184,28 @@ def get_miniapp_blueprint(config: ConfigManager, prefix: str): ) def save_settings(n_clicks, values, keys): if n_clicks > 0: + user = getattr(flask.g, "user", None) + + if user is None: + return ( + dbc.Alert( + "Вы не авторизованы!", + color="danger", + duration=10000, + ), + "-", + ) + + if not asyncio.run(roles.check_admin_permission(user["id"])): + return ( + dbc.Alert( + "Вы не администратор!", + color="danger", + duration=10000, + ), + "-", + ) + # TODO: добавить валидацию значений updated_settings = {} @@ -178,12 +213,18 @@ def get_miniapp_blueprint(config: ConfigManager, prefix: str): key: str = id_dict["key"] if prefix: key = key.removeprefix(f"{prefix}-") - updated_settings[key] = value + + meta = config.get_meta(key) + + if meta["type"] == "password": + if value: # Only update if a new value is provided + updated_settings[key] = value + else: + updated_settings[key] = value config.mass_set(updated_settings) config.save() - # locale.setlocale(locale.LC_TIME, "ru_RU.UTF-8") now = datetime.datetime.now() date_str = now.strftime("%H:%M:%S") diff --git a/src/ocab_modules/ocab_modules/standard/miniapp/dash_telegram_auth.py b/src/ocab_modules/ocab_modules/standard/miniapp/dash_telegram_auth.py index 6bf5c9d..5137770 100644 --- a/src/ocab_modules/ocab_modules/standard/miniapp/dash_telegram_auth.py +++ b/src/ocab_modules/ocab_modules/standard/miniapp/dash_telegram_auth.py @@ -1,13 +1,104 @@ import flask from aiogram.utils.web_app import safe_parse_webapp_init_data from dash import Dash -from dash_extensions.enrich import Input +from dash_extensions.enrich import Input, Output from flask import request +# TODO: добавить прокидывание BASE_PATH, т.к. это параметр из настроек + +WEBAPP_LOADER_TEMPLATE = """ + + + + + + OCAB + + + + + +
Loading...
+ + + + +""" + def get_auth_server(bot_token: str): server = flask.Flask(__name__) + @server.route("/") + @server.route("/") + def webapp_loader(rest=None): + return flask.Response(WEBAPP_LOADER_TEMPLATE, mimetype="text/html") + @server.before_request def add_auth_data(): init_data = request.cookies.get("tg_init_data") @@ -21,13 +112,35 @@ def get_auth_server(bot_token: str): return server -def setup_auth_clientcallback(app: Dash): +def setup_auth_clientcallbacks(app: Dash): app.clientside_callback( """ - function(n_inervals) { - const tg = window.Telegram.WebApp; - document.cookie = `tg_init_data=${JSON.stringify(tg.initData)}; path=/`; + function(n_intervals) { + if (window.webAppData) { + return window.webAppData; + } + + function receiveMessage(event) { + if (event.data.type === 'webAppData') { + window.webAppData = event.data.webApp; + window.removeEventListener('message', receiveMessage); + } + } + + window.addEventListener('message', receiveMessage, false); + + return window.dash_clientside.no_update; } """, - Input("init-telegram-interval", "n_intervals"), + Output("hidden-div", "children"), + Input("interval-component", "n_intervals"), + ) + + app.clientside_callback( + """ + function(pathname) { + window.parent.postMessage({ type: 'iframe-url-changed', pathname }, '*'); + } + """, + Input("url", "pathname"), ) diff --git a/src/ocab_modules/ocab_modules/standard/miniapp/lib.py b/src/ocab_modules/ocab_modules/standard/miniapp/lib.py index 64273cd..9768ae9 100644 --- a/src/ocab_modules/ocab_modules/standard/miniapp/lib.py +++ b/src/ocab_modules/ocab_modules/standard/miniapp/lib.py @@ -1,26 +1,30 @@ +import asyncio from collections import OrderedDict from typing import TYPE_CHECKING import dash import dash_bootstrap_components as dbc +import flask from dash_extensions.enrich import DashBlueprint, DashProxy, Input, Output, dcc, html from dash_extensions.pages import setup_page_components -from ocab_core.modules_system.public_api import get_module +from ocab_core.modules_system.public_api import get_module, log -from .dash_telegram_auth import get_auth_server, setup_auth_clientcallback +from .dash_telegram_auth import get_auth_server, setup_auth_clientcallbacks if TYPE_CHECKING: from ocab_modules.standard.config import IConfig + from ocab_modules.standard.roles import Roles as IRoles pages = OrderedDict() -def register_page(name, path, blueprint, prefix=""): +def register_page(name, path, blueprint, prefix="", role="USER"): pages[path] = { "name": name, "blueprint": blueprint, "prefix": prefix, + "role": role, } @@ -33,9 +37,14 @@ def register_home_page(): register_home_page() config: "IConfig" = get_module("standard.config", "config") +Roles: "type[IRoles]" = get_module("standard.roles", "Roles") def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: + log(requests_pathname_prefix) + + real_prefix = f"{requests_pathname_prefix}_internal/" + server = get_auth_server(config.get("core::token")) app = DashProxy( @@ -47,10 +56,13 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: dbc.icons.BOOTSTRAP, ], external_scripts=[ + # "https://telegram.org/js/telegram-web-app.js" - ], # Add Telegram Mini Apps script to + ], server=server, - requests_pathname_prefix=requests_pathname_prefix, + requests_pathname_prefix=real_prefix, + routes_pathname_prefix="/_internal/", + # requests_pathname_prefix=requests_pathname_prefix, meta_tags=[ {"name": "viewport", "content": "width=device-width, initial-scale=1"}, ], @@ -63,30 +75,8 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: # Register pages for path, page in pages.items(): - # dash.register_page(page["name"], path=path, layout=page["layout"]) page["blueprint"].register(app, path, prefix=page["prefix"]) - # Create sidebar - sidebar = dbc.Offcanvas( - id="offcanvas", - title="Меню", - is_open=False, - children=[ - dbc.Nav( - [ - dbc.NavLink( - page["name"], - href=f"{requests_pathname_prefix}{path.lstrip('/')}", - id={"type": "nav-link", "index": path}, - ) - for path, page in pages.items() - ], - vertical=True, - pills=True, - ), - ], - ) - # Create navbar navbar = dbc.Navbar( dbc.Container( @@ -104,24 +94,67 @@ def create_dash_app(requests_pathname_prefix: str = None) -> dash.Dash: dark=True, ) - # Define app layout - app.layout = html.Div( - [ - dcc.Location(id="url", refresh=False), - dcc.Interval( - id="init-telegram-interval", - interval=100, - n_intervals=0, - max_intervals=1, - ), - navbar, - sidebar, - dash.page_container, - setup_page_components(), - ] - ) + roles = Roles() - setup_auth_clientcallback(app) + def create_layout(): + user = getattr(flask.g, "user", None) + + if not user: + return html.Div() + + user_id = user["id"] + user_permission = asyncio.run(roles.get_user_permission(user_id)) or "USER" + + available_pages = { + path: page + for path, page in pages.items() + if (isinstance(page["role"], list) and user_permission in page["role"]) + or page["role"] == user_permission + or page["role"] == "USER" + } + + # Create sidebar + sidebar = dbc.Offcanvas( + id="offcanvas", + title="Меню", + is_open=False, + children=[ + dbc.Nav( + [ + dbc.NavLink( + page["name"], + href=f"{real_prefix}/{path.lstrip('/')}", + id={"type": "nav-link", "index": path}, + ) + for path, page in available_pages.items() + ], + vertical=True, + pills=True, + ), + ], + ) + + layout = html.Div( + [ + dcc.Location(id="url", refresh=False), + dcc.Interval( + id="init-telegram-interval", + interval=100, + n_intervals=0, + max_intervals=1, + ), + navbar, + sidebar, + dash.page_container, + setup_page_components(), + ] + ) + + return layout + + app.layout = create_layout + + setup_auth_clientcallbacks(app) # Открытие на кнопку меню app.clientside_callback(