import logging import os from typing import Any, Callable, Dict from descope import AuthException, DescopeClient from nicegui import Client, app, ui _descope_id = os.environ.get('DESCOPE_PROJECT_ID', '') try: descope_client = DescopeClient(project_id=_descope_id) except Exception as error: logging.exception("failed to initialize.") def login_form() -> ui.element: """Places and returns the Descope login form.""" with ui.card().classes('w-96 mx-auto'): return ui.element('descope-wc').props(f'project-id="{_descope_id}" flow-id="sign-up-or-in"') \ .on('success', lambda e: app.storage.user.update({'descope': e.args['detail']['user']})) def about() -> Dict[str, Any]: """Returns the user's Descope profile.""" infos = app.storage.user['descope'] if not infos: raise Exception('User is not logged in.') async def logout() -> None: """Logs the user out.""" result = await ui.run_javascript('return await sdk.logout()', respond=True) if result['code'] != 200: logging.error(f'Logout failed: {result}') ui.notify('Logout failed', type='negative') else: app.storage.user['descope'] = None ui.open('/login') class page(ui.page): def __init__(self, path): """A page that requires the user to be logged in. It allows the same parameters as ui.page, but adds a login check. As recommended by Descope this is done via JavaScript and allows to use Flows. But this means that the page has already awaited the client connection. So `ui.add_head_html` will not work. """ super().__init__(path) def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]: async def content(client: Client): ui.add_head_html('') ui.add_head_html('') ui.add_body_html(''' ''') await client.connected() token = await ui.run_javascript('return sessionToken && !sdk.isJwtExpired(sessionToken) ? sessionToken : null;') if token and self._verify(token): if self.path == '/login': await self.refresh_token() ui.open('/') else: func() else: if self.path != '/login': ui.open('/login') else: ui.timer(30, self.refresh_token) func() return super().__call__(content) @staticmethod def _verify(token: str) -> bool: try: descope_client.validate_session(session_token=token) return True except AuthException: logging.exception("Could not validate user session.") ui.notify('Wrong username or password', type='negative') return False @staticmethod async def refresh_token() -> None: await ui.run_javascript('sdk.refresh()', respond=False) def login_page(func: Callable[..., Any]) -> Callable[..., Any]: """Marks the special page that will contain the login form.""" return page('/login')(func)