jaaql-middleware-python 4.34.2__tar.gz → 5.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/PKG-INFO +1 -1
  2. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/constants.py +3 -1
  3. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/documentation/documentation_internal.py +44 -6
  4. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/exceptions/jaaql_interpretable_handled_errors.py +15 -1
  5. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/base_controller.py +14 -1
  6. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/base_model.py +35 -10
  7. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/controller.py +17 -5
  8. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/generated_queries.py +1 -0
  9. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/model.py +570 -142
  10. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/01.install_domains.generated.sql +2 -0
  11. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/02.install_super_user.exceptions.sql +3 -5
  12. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/03.install_super_user.handwritten.sql +40 -0
  13. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/04.install_jaaql_data_structures.generated.sql +1 -0
  14. jaaql_middleware_python-5.1.0/jaaql/services/migrations_manager_service.py +551 -0
  15. jaaql_middleware_python-5.1.0/jaaql/utilities/bootstrap_secrets.py +33 -0
  16. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/utils_no_project_imports.py +1 -0
  17. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql_middleware_python.egg-info/PKG-INFO +1 -1
  18. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql_middleware_python.egg-info/SOURCES.txt +1 -0
  19. jaaql_middleware_python-4.34.2/jaaql/services/migrations_manager_service.py +0 -56
  20. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/LICENSE.txt +0 -0
  21. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/README.md +0 -0
  22. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/__init__.py +0 -0
  23. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/config/__init__.py +0 -0
  24. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/config/config-docker.ini +0 -0
  25. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/config/config-test.ini +0 -0
  26. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/config/config.ini +0 -0
  27. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/config_constants.py +0 -0
  28. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/db/__init__.py +0 -0
  29. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/db/db_interface.py +0 -0
  30. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/db/db_pg_interface.py +0 -0
  31. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/db/db_utils.py +0 -0
  32. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/db/db_utils_no_circ.py +0 -0
  33. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/documentation/__init__.py +0 -0
  34. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/documentation/documentation_public.py +0 -0
  35. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/documentation/documentation_shared.py +0 -0
  36. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/email/__init__.py +0 -0
  37. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/email/email_manager.py +0 -0
  38. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/email/email_manager_service.py +0 -0
  39. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/email/patch_ems.py +0 -0
  40. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/exceptions/__init__.py +0 -0
  41. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/exceptions/custom_http_status.py +0 -0
  42. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/exceptions/http_status_exception.py +0 -0
  43. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/exceptions/not_yet_implement_exception.py +0 -0
  44. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/generated_constants.py +0 -0
  45. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/interpreter/__init__.py +0 -0
  46. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/interpreter/interpret_jaaql.py +0 -0
  47. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/jaaql.py +0 -0
  48. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/migrations/__init__.py +0 -0
  49. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/migrations/migrations.py +0 -0
  50. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/__init__.py +0 -0
  51. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/controller_interface.py +0 -0
  52. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/exception_queries.py +0 -0
  53. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/handmade_queries.py +0 -0
  54. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/model_interface.py +0 -0
  55. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/mvc/response.py +0 -0
  56. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/openapi/__init__.py +0 -0
  57. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/openapi/swagger_documentation.py +0 -0
  58. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/patch.py +0 -0
  59. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/05.install_static_data.generated.sql +0 -0
  60. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/06.install_jaaql.exceptions.sql +0 -0
  61. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/ZZZZ.generated_functions_views_and_permissions.sql +0 -0
  62. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/ZZZZ.reset_references.sql +0 -0
  63. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/scripts/swagger_template.html +0 -0
  64. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/services/__init__.py +0 -0
  65. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/services/cached_canned_query_service.py +0 -0
  66. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/services/patch_mms.py +0 -0
  67. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/services/patch_shared_var_service.py +0 -0
  68. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/services/shared_var_service.py +0 -0
  69. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/__init__.py +0 -0
  70. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/cron.py +0 -0
  71. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/crypt_utils.py +0 -0
  72. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/options.py +0 -0
  73. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/utils.py +0 -0
  74. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql/utilities/vault.py +0 -0
  75. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql_middleware_python.egg-info/dependency_links.txt +0 -0
  76. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql_middleware_python.egg-info/requires.txt +0 -0
  77. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/jaaql_middleware_python.egg-info/top_level.txt +0 -0
  78. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/setup.cfg +0 -0
  79. {jaaql_middleware_python-4.34.2 → jaaql_middleware_python-5.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jaaql-middleware-python
3
- Version: 4.34.2
3
+ Version: 5.1.0
4
4
  Summary: The jaaql package, allowing for rapid development and deployment of RESTful HTTP applications
5
5
  Home-page: https://github.com/JAAQL/JAAQL-middleware-python
6
6
  Author: Software Quality Measurement and Improvement bv
@@ -91,6 +91,8 @@ VAULT_KEY__super_db_credentials = "super_db_credentials"
91
91
  VAULT_KEY__jaaql_lookup_connection = "jaaql_lookup_connection"
92
92
  VAULT_KEY__allow_jaaql_uninstall = "Allow jaaql uninstall"
93
93
  VAULT_KEY__jaaql_db_password = "Jaaql DB Password"
94
+ VAULT_KEY__postgres_bootstrap_password = "postgres_bootstrap_password"
95
+ VAULT_KEY__keycloak_realm_admin_secret = "keycloak_realm_admin_secret"
94
96
 
95
97
  # Used for re-installation
96
98
  VAULT_KEY__db_connection_string = "db_connection_string"
@@ -208,5 +210,5 @@ ROLE__postgres = "postgres"
208
210
 
209
211
  PROTOCOL__postgres = "postgresql://"
210
212
 
211
- VERSION = "4.34.2"
213
+ VERSION = "5.1.0"
212
214
 
@@ -367,12 +367,40 @@ DOCUMENTATION__oidc_exchange_code = SwaggerDocumentation(
367
367
  name="Fetch OIDC code",
368
368
  description="Exchanges OIDC auth code for auth token, returns the token",
369
369
  method=REST__GET,
370
- arguments=SwaggerArgumentResponse(
371
- name="response",
372
- description="The OIDC response JWT object",
373
- arg_type=str,
374
- example=["eyJ..."]
375
- ),
370
+ arguments=[
371
+ SwaggerArgumentResponse(
372
+ name="response",
373
+ description="The OIDC response JWT object (FAPI/JARM mode)",
374
+ arg_type=str,
375
+ required=False,
376
+ condition="When USE_FAPI_ADVANCED or standard JARM is enabled",
377
+ example=["eyJ..."]
378
+ ),
379
+ SwaggerArgumentResponse(
380
+ name="code",
381
+ description="The authorization code (basic OIDC mode)",
382
+ arg_type=str,
383
+ required=False,
384
+ condition="When USE_OIDC_BASIC is enabled",
385
+ example=["abc123"]
386
+ ),
387
+ SwaggerArgumentResponse(
388
+ name="state",
389
+ description="The state parameter (basic OIDC mode)",
390
+ arg_type=str,
391
+ required=False,
392
+ condition="When USE_OIDC_BASIC is enabled",
393
+ example=["xyz789"]
394
+ ),
395
+ SwaggerArgumentResponse(
396
+ name="session_state",
397
+ description="The session state parameter (Azure AD / OpenID Connect session management)",
398
+ arg_type=str,
399
+ required=False,
400
+ condition="When the identity provider includes session management",
401
+ example=["0022d22a-3a6c-953c-be05-80a155fda337"]
402
+ )
403
+ ],
376
404
  response=SwaggerFlatResponse(
377
405
  description="URL",
378
406
  code=HTTPStatus.FOUND,
@@ -381,6 +409,16 @@ DOCUMENTATION__oidc_exchange_code = SwaggerDocumentation(
381
409
  )
382
410
  )
383
411
 
412
+ DOCUMENTATION__resend_verify_email = SwaggerDocumentation(
413
+ tags="oidc",
414
+ security=False,
415
+ methods=SwaggerMethod(
416
+ name="Resend verification email",
417
+ description="Triggers Keycloak to resend a verification email for a given email address",
418
+ method=REST__POST,
419
+ )
420
+ )
421
+
384
422
  DOCUMENTATION__jwks = SwaggerDocumentation(
385
423
  tags="jwks",
386
424
  security=False,
@@ -127,10 +127,24 @@ class UserUnauthorized(JaaqlInterpretableHandledError):
127
127
  )
128
128
 
129
129
 
130
- class UnhandledJaaqlServerError(JaaqlInterpretableHandledError):
130
+ class EmailConfirmationRequired(JaaqlInterpretableHandledError):
131
131
  def __init__(self, descriptor=None):
132
132
  super().__init__(
133
133
  error_code=1010,
134
+ http_response_code=403,
135
+ table_name=None,
136
+ message="Your email confirmation grace period has expired. Please confirm your email to continue.",
137
+ column_name=None,
138
+ _set=None,
139
+ index=None,
140
+ descriptor=descriptor
141
+ )
142
+
143
+
144
+ class UnhandledJaaqlServerError(JaaqlInterpretableHandledError):
145
+ def __init__(self, descriptor=None):
146
+ super().__init__(
147
+ error_code=1099,
134
148
  http_response_code=500,
135
149
  table_name=None,
136
150
  message="An unhandled exception has occurred with JAAQL.",
@@ -35,6 +35,7 @@ from jaaql.exceptions.http_status_exception import *
35
35
  ARG__http_inputs = "http_inputs"
36
36
  ARG__account_id = "account_id"
37
37
  ARG__ip_address = "ip_address"
38
+ ARG__request_headers = "request_headers"
38
39
  ARG__response = "response"
39
40
  ARG__username = "username"
40
41
  ARG__profiler = "profiler"
@@ -496,7 +497,16 @@ class BaseJAAQLController:
496
497
  if auth_cookie is not None:
497
498
  security_key = auth_cookie
498
499
 
499
- if swagger_documentation.security:
500
+ easyauth_resolved = False
501
+ if self.model.use_easyauth and security_key is None:
502
+ easyauth_principal_id = request.headers.get("X-MS-CLIENT-PRINCIPAL-ID")
503
+ if easyauth_principal_id:
504
+ account_id, username, ip_id, is_public, remember_me = \
505
+ self.model.authenticate_via_easyauth(request.headers, ip_addr, jaaql_resp)
506
+ easyauth_resolved = True
507
+ self.perform_profile(request_id, "EasyAuth")
508
+
509
+ if swagger_documentation.security and not easyauth_resolved:
500
510
  bypass_super = request.headers.get(HEADER__security_bypass)
501
511
  bypass_jaaql = request.headers.get(HEADER__security_bypass_jaaql)
502
512
  bypass_user = request.headers.get(HEADER__security_specify_user)
@@ -566,6 +576,9 @@ class BaseJAAQLController:
566
576
  if ARG__ip_address in inspect.getfullargspec(view_func_local).args:
567
577
  supply_dict[ARG__ip_address] = ip_addr
568
578
 
579
+ if ARG__request_headers in inspect.getfullargspec(view_func_local).args:
580
+ supply_dict[ARG__request_headers] = request.headers
581
+
569
582
  if ARG__response in inspect.getfullargspec(view_func_local).args:
570
583
  supply_dict[ARG__response] = jaaql_resp
571
584
 
@@ -18,6 +18,7 @@ from jaaql.config_constants import *
18
18
  from jaaql.email.email_manager import EmailManager
19
19
  from os.path import dirname
20
20
  from jaaql.services.cached_canned_query_service import CachedCannedQueryService
21
+ from jaaql.utilities.bootstrap_secrets import get_or_seed_vault_secret
21
22
  import threading
22
23
 
23
24
  import uuid
@@ -236,6 +237,19 @@ class BaseJAAQLModel:
236
237
 
237
238
  self.application_url = os.environ.get("SERVER_ADDRESS", "")
238
239
  self.use_fapi_advanced = os.environ.get("USE_FAPI_ADVANCED", "").lower() == "true"
240
+ self.use_oidc_basic = os.environ.get("USE_OIDC_BASIC", "").lower() == "true"
241
+ if self.use_oidc_basic:
242
+ # Basic OIDC mode intentionally disables the advanced FAPI/JARM/PAR flow.
243
+ self.use_fapi_advanced = False
244
+
245
+ self.use_easyauth = os.environ.get("USE_EASYAUTH", "").lower() == "true"
246
+ if self.use_easyauth:
247
+ if self.use_oidc_basic or self.use_fapi_advanced:
248
+ raise Exception("USE_EASYAUTH cannot be combined with USE_OIDC_BASIC or USE_FAPI_ADVANCED")
249
+ print("EasyAuth mode enabled. Container MUST be behind Azure EasyAuth (App Service or Container Apps).")
250
+ self.easyauth_provider = os.environ.get("EASYAUTH_PROVIDER", "azure")
251
+ self.easyauth_tenant = os.environ.get("EASYAUTH_TENANT", "easyauth")
252
+ self.easyauth_provisioned = False
239
253
 
240
254
  self.fapi_pem = None
241
255
  self.fapi_cert = None
@@ -293,16 +307,27 @@ class BaseJAAQLModel:
293
307
 
294
308
  self.reload_lock = threading.Lock()
295
309
 
296
- if not self.vault.has_obj(VAULT_KEY__jaaql_local_access_key):
297
- self.vault.insert_obj(VAULT_KEY__jaaql_local_access_key, str(uuid.uuid4()))
298
-
299
- if not self.vault.has_obj(VAULT_KEY__super_local_access_key):
300
- self.vault.insert_obj(VAULT_KEY__super_local_access_key, str(uuid.uuid4()))
301
-
302
- self.local_jaaql_access_key = os.environ.get(ENVIRON__JAAQL__JAAQL_BYPASS_KEY,
303
- self.vault.get_obj(VAULT_KEY__super_local_access_key) if self.is_container else "00000-00000")
304
- self.local_super_access_key = os.environ.get(ENVIRON__JAAQL__SUPER_BYPASS_KEY,
305
- self.vault.get_obj(VAULT_KEY__super_local_access_key) if self.is_container else "00000-00000")
310
+ jaaql_bypass_key = get_or_seed_vault_secret(
311
+ self.vault,
312
+ VAULT_KEY__jaaql_local_access_key,
313
+ ENVIRON__JAAQL__JAAQL_BYPASS_KEY,
314
+ generate_if_missing=True
315
+ )
316
+ super_bypass_key = get_or_seed_vault_secret(
317
+ self.vault,
318
+ VAULT_KEY__super_local_access_key,
319
+ ENVIRON__JAAQL__SUPER_BYPASS_KEY,
320
+ generate_if_missing=True
321
+ )
322
+ get_or_seed_vault_secret(
323
+ self.vault,
324
+ VAULT_KEY__keycloak_realm_admin_secret,
325
+ "KEYCLOAK_REALM_ADMIN_SECRET",
326
+ generate_if_missing=False
327
+ )
328
+
329
+ self.local_jaaql_access_key = jaaql_bypass_key if self.is_container else "00000-00000"
330
+ self.local_super_access_key = super_bypass_key if self.is_container else "00000-00000"
306
331
 
307
332
  if self.vault.has_obj(VAULT_KEY__jaaql_lookup_connection):
308
333
  if self.is_container:
@@ -1,3 +1,5 @@
1
+ from http import HTTPStatus
2
+
1
3
  from jaaql.mvc.exception_queries import SECURITY_EVENT_TYPE__create, SECURITY_EVENT_TYPE__delete, \
2
4
  SECURITY_EVENT_TYPE__reset
3
5
  from jaaql.mvc.model import JAAQLModel
@@ -10,7 +12,7 @@ from jaaql.db.db_interface import DBInterface
10
12
  from flask import request
11
13
  import queue
12
14
 
13
- from jaaql.utilities.utils_no_project_imports import COOKIE_OIDC
15
+ from jaaql.utilities.utils_no_project_imports import COOKIE_OIDC, COOKIE_OIDC_RETURN
14
16
 
15
17
 
16
18
  class JAAQLController(BaseJAAQLController):
@@ -171,16 +173,26 @@ class JAAQLController(BaseJAAQLController):
171
173
  return self.model.fetch_user_registries_for_tenant(http_inputs)
172
174
 
173
175
  @self.publish_route('/oidc-redirect-url', DOCUMENTATION__oidc_redirect_url)
174
- def fetch_redirect_url(http_inputs: dict, response: JAAQLResponse):
175
- self.model.fetch_redirect_uri(http_inputs, response)
176
+ def fetch_redirect_url(http_inputs: dict, response: JAAQLResponse, request_headers=None, ip_address=None):
177
+ self.model.fetch_redirect_uri(http_inputs, response, request_headers=request_headers, ip_address=ip_address)
176
178
 
177
179
  @self.publish_route(ENDPOINT__oidc_get_token, DOCUMENTATION__oidc_exchange_code)
178
180
  def exchange_auth_code(http_inputs: dict, ip_address: str, response: JAAQLResponse):
179
181
  try:
180
182
  self.model.exchange_auth_code(http_inputs, request.cookies.get(COOKIE_OIDC), ip_address, response)
181
- except: # Sometimes this can fail when the user didn't exist before
183
+ except Exception as e:
184
+ import traceback
185
+ traceback.print_exc()
186
+ print(f"OIDC code exchange failed: {e}")
187
+ # Redirect to the saved return URL if available (survives OIDC cookie consumption),
188
+ # otherwise fall back to the app root
189
+ return_url = request.cookies.get(COOKIE_OIDC_RETURN)
182
190
  response.response_code = HTTPStatus.FOUND
183
- response.raw_headers["Location"] = self.model.url.replace("_", "localhost")
191
+ response.raw_headers["Location"] = return_url if return_url else self.model.url.replace("_", "localhost")
192
+
193
+ @self.publish_route('/resend-verify-email', DOCUMENTATION__resend_verify_email)
194
+ def resend_verify_email(http_inputs: dict):
195
+ self.model.resend_verification_email(http_inputs.get("email", ""))
184
196
 
185
197
  @self.publish_route('/.well-known/jwks', DOCUMENTATION__jwks)
186
198
  def fetch_jwks():
@@ -1309,6 +1309,7 @@ KG__account__sub = "sub"
1309
1309
  KG__account__username = "username"
1310
1310
  KG__account__email = "email"
1311
1311
  KG__account__email_verified = "email_verified"
1312
+ KG__account__created_timestamp = "created_timestamp"
1312
1313
  KG__account__deletion_timestamp = "deletion_timestamp"
1313
1314
  KG__account__provider = "provider"
1314
1315
  KG__account__tenant = "tenant"