user.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. import logging
  2. import os
  3. from typing import Any, Callable, Dict
  4. from descope import AuthException, DescopeClient
  5. from nicegui import app, helpers, 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. """Create and return 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. """Return the user's Descope profile.
  18. This function can only be used after the user has logged in.
  19. """
  20. return app.storage.user['descope']
  21. async def logout() -> None:
  22. """Logout the user."""
  23. result = await ui.run_javascript('return await sdk.logout()')
  24. if result['code'] == 200:
  25. app.storage.user['descope'] = None
  26. else:
  27. logging.error(f'Logout failed: {result}')
  28. ui.notify('Logout failed', type='negative')
  29. ui.navigate.to(page.LOGIN_PATH)
  30. class page(ui.page):
  31. """A page that requires the user to be logged in.
  32. It allows the same parameters as ui.page, but adds a login check.
  33. As recommended by Descope, this is done via JavaScript and allows to use Flows.
  34. But this means that the page has already awaited the client connection.
  35. So `ui.add_head_html` will not work.
  36. """
  37. SESSION_TOKEN_REFRESH_INTERVAL = 30
  38. LOGIN_PATH = '/login'
  39. def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
  40. async def content():
  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(f'''
  44. <script>
  45. const sdk = Descope({{ projectId: '{DESCOPE_ID}', persistTokens: true, autoRefresh: true }});
  46. const sessionToken = sdk.getSessionToken()
  47. </script>
  48. ''')
  49. await ui.context.client.connected()
  50. if await self._is_logged_in():
  51. if self.path == self.LOGIN_PATH:
  52. self._refresh()
  53. ui.navigate.to('/')
  54. return
  55. else:
  56. if self.path != self.LOGIN_PATH:
  57. ui.navigate.to(self.LOGIN_PATH)
  58. return
  59. ui.timer(self.SESSION_TOKEN_REFRESH_INTERVAL, self._refresh)
  60. if helpers.is_coroutine_function(func):
  61. await func()
  62. else:
  63. func()
  64. return super().__call__(content)
  65. @staticmethod
  66. async def _is_logged_in() -> bool:
  67. if not app.storage.user.get('descope'):
  68. return False
  69. token = await ui.run_javascript('return sessionToken && !sdk.isJwtExpired(sessionToken) ? sessionToken : null;')
  70. if not token:
  71. return False
  72. try:
  73. descope_client.validate_session(session_token=token)
  74. return True
  75. except AuthException:
  76. logging.exception('Could not validate user session.')
  77. ui.notify('Wrong username or password', type='negative')
  78. return False
  79. @staticmethod
  80. def _refresh() -> None:
  81. ui.run_javascript('sdk.refresh()')
  82. def login_page(func: Callable[..., Any]) -> Callable[..., Any]:
  83. """Marks the special page that will contain the login form."""
  84. return page(page.LOGIN_PATH)(func)