user.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  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 AuthException as ex:
  10. print(ex.error_message)
  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. return infos
  22. async def logout() -> None:
  23. """Logs the user out."""
  24. result = await ui.run_javascript('return await sdk.logout()', respond=True)
  25. if result['code'] != 200:
  26. logging.error(f'Logout failed: {result}')
  27. ui.notify('Logout failed', type='negative')
  28. else:
  29. app.storage.user['descope'] = None
  30. ui.open('/login')
  31. class page(ui.page):
  32. def __init__(self, path):
  33. """A page that requires the user to be logged in.
  34. It allows the same parameters as ui.page, but adds a login check.
  35. As recommended by Descope this is done via JavaScript and allows to use Flows.
  36. But this means that the page has already awaited the client connection.
  37. So `ui.add_head_html` will not work.
  38. """
  39. super().__init__(path)
  40. def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
  41. async def content(client: Client):
  42. ui.add_head_html('<script src="https://unpkg.com/@descope/web-component@latest/dist/index.js"></script>')
  43. ui.add_head_html('<script src="https://unpkg.com/@descope/web-js-sdk@latest/dist/index.umd.js"></script>')
  44. ui.add_body_html('''
  45. <script>
  46. const sdk = Descope({ projectId: \'''' + _descope_id + '''\', persistTokens: true, autoRefresh: true });
  47. const sessionToken = sdk.getSessionToken()
  48. </script>
  49. ''')
  50. await client.connected()
  51. token = await ui.run_javascript('return sessionToken && !sdk.isJwtExpired(sessionToken) ? sessionToken : null;')
  52. if token and self._verify(token):
  53. if self.path == '/login':
  54. await self._refresh()
  55. ui.open('/')
  56. else:
  57. func()
  58. else:
  59. if self.path != '/login':
  60. ui.open('/login')
  61. else:
  62. ui.timer(30, self._refresh)
  63. func()
  64. return super().__call__(content)
  65. @staticmethod
  66. def _verify(token: str) -> bool:
  67. try:
  68. descope_client.validate_session(session_token=token)
  69. return True
  70. except AuthException:
  71. logging.exception("Could not validate user session.")
  72. ui.notify('Wrong username or password', type='negative')
  73. return False
  74. @staticmethod
  75. async def _refresh() -> None:
  76. await ui.run_javascript('sdk.refresh()', respond=False)
  77. def login_page(func: Callable[..., Any]) -> Callable[..., Any]:
  78. """Marks the special page that will contain the login form."""
  79. return page('/login')(func)