user.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import logging
  2. import os
  3. from typing import Any, Callable, Dict
  4. from descope import AuthException, DescopeClient
  5. from nicegui import Client, app, ui
  6. _descope_id = os.environ.get('DESCOPE_PROJECT_ID', '')
  7. try:
  8. descope_client = DescopeClient(project_id=_descope_id)
  9. except Exception as error:
  10. logging.exception("failed to initialize.")
  11. def login_form() -> ui.element:
  12. """Places and returns the Descope login form."""
  13. with ui.card().classes('w-96 mx-auto'):
  14. return ui.element('descope-wc').props(f'project-id="{_descope_id}" flow-id="sign-up-or-in"') \
  15. .on('success', lambda e: app.storage.user.update({'descope': e.args['detail']['user']}))
  16. def about() -> Dict[str, Any]:
  17. """Returns the user's Descope profile."""
  18. infos = app.storage.user['descope']
  19. if not infos:
  20. raise Exception('User is not logged in.')
  21. async def logout() -> None:
  22. """Logs the user out."""
  23. result = await ui.run_javascript('return await sdk.logout()', respond=True)
  24. if result['code'] != 200:
  25. logging.error(f'Logout failed: {result}')
  26. ui.notify('Logout failed', type='negative')
  27. else:
  28. app.storage.user['descope'] = None
  29. ui.open('/login')
  30. class page(ui.page):
  31. def __init__(self, path):
  32. """A page that requires the user to be logged in.
  33. It allows the same parameters as ui.page, but adds a login check.
  34. As recommended by Descope this is done via JavaScript and allows to use Flows.
  35. But this means that the page has already awaited the client connection.
  36. So `ui.add_head_html` will not work.
  37. """
  38. super().__init__(path)
  39. def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
  40. async def content(client: Client):
  41. ui.add_head_html('<script src="https://unpkg.com/@descope/web-component@latest/dist/index.js"></script>')
  42. ui.add_head_html('<script src="https://unpkg.com/@descope/web-js-sdk@latest/dist/index.umd.js"></script>')
  43. ui.add_body_html('''
  44. <script>
  45. const sdk = Descope({ projectId: \'''' + _descope_id + '''\', persistTokens: true, autoRefresh: true });
  46. const sessionToken = sdk.getSessionToken()
  47. </script>
  48. ''')
  49. await client.connected()
  50. token = await ui.run_javascript('return sessionToken && !sdk.isJwtExpired(sessionToken) ? sessionToken : null;')
  51. if token and self._verify(token):
  52. if self.path == '/login':
  53. await self.refresh_token()
  54. ui.open('/')
  55. else:
  56. func()
  57. else:
  58. if self.path != '/login':
  59. ui.open('/login')
  60. else:
  61. ui.timer(30, self.refresh_token)
  62. func()
  63. return super().__call__(content)
  64. @staticmethod
  65. def _verify(token: str) -> bool:
  66. try:
  67. descope_client.validate_session(session_token=token)
  68. return True
  69. except AuthException:
  70. logging.exception("Could not validate user session.")
  71. ui.notify('Wrong username or password', type='negative')
  72. return False
  73. @staticmethod
  74. async def refresh_token() -> None:
  75. await ui.run_javascript('sdk.refresh()', respond=False)
  76. def login_page(func: Callable[..., Any]) -> Callable[..., Any]:
  77. """Marks the special page that will contain the login form."""
  78. return page('/login')(func)