zrb 1.0.0b9__py3-none-any.whl → 1.1.0__py3-none-any.whl

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 (88) hide show
  1. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
  2. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
  4. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +131 -2
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +128 -5
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +297 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +81 -13
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -3
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +8 -1
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +56 -12
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +3 -3
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +19 -8
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
  30. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
  31. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
  32. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
  33. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +277 -44
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +66 -0
  35. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
  36. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
  37. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
  38. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
  39. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
  40. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
  41. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
  42. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
  43. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
  44. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
  45. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
  46. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
  47. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
  48. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
  49. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +6 -1
  50. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  51. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
  52. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
  53. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
  54. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
  55. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
  56. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
  57. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
  58. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
  59. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +15 -0
  60. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
  61. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
  62. zrb/runner/web_route/refresh_token_api_route.py +1 -1
  63. zrb/runner/web_route/static/refresh-token.template.js +9 -0
  64. zrb/runner/web_route/static/static_route.py +1 -1
  65. zrb/task/base_task.py +10 -10
  66. zrb/util/codemod/modification_mode.py +3 -0
  67. zrb/util/codemod/modify_class.py +58 -0
  68. zrb/util/codemod/modify_class_parent.py +68 -0
  69. zrb/util/codemod/modify_class_property.py +128 -0
  70. zrb/util/codemod/modify_dict.py +75 -0
  71. zrb/util/codemod/modify_function.py +65 -0
  72. zrb/util/codemod/modify_function_call.py +68 -0
  73. zrb/util/codemod/modify_method.py +88 -0
  74. zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
  75. zrb/util/file.py +3 -2
  76. zrb/util/load.py +13 -7
  77. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
  78. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/RECORD +80 -46
  79. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
  80. zrb/util/codemod/append_code_to_class.py +0 -35
  81. zrb/util/codemod/append_code_to_function.py +0 -38
  82. zrb/util/codemod/append_code_to_method.py +0 -55
  83. zrb/util/codemod/append_key_to_dict.py +0 -51
  84. zrb/util/codemod/append_param_to_function_call.py +0 -39
  85. zrb/util/codemod/prepend_parent_to_class.py +0 -38
  86. zrb/util/codemod/prepend_property_to_class.py +0 -55
  87. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
  88. {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,69 @@
1
+ """create_permissions
2
+
3
+ Revision ID: 8ed025bcc845
4
+ Revises: 3093c7336477
5
+ Create Date: 2025-02-08 19:09:14.536559
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ import sqlalchemy as sa
12
+ import sqlmodel # 🔥 FastApp Modification
13
+ from alembic import op
14
+ from module.auth.migration_metadata import metadata
15
+
16
+ # revision identifiers, used by Alembic.
17
+ revision: str = "8ed025bcc845"
18
+ down_revision: Union[str, None] = "3093c7336477"
19
+ branch_labels: Union[str, Sequence[str], None] = None
20
+ depends_on: Union[str, Sequence[str], None] = None
21
+
22
+
23
+ def upgrade() -> None:
24
+ op.bulk_insert(
25
+ metadata.tables["permissions"],
26
+ [
27
+ # permission
28
+ {"name": "permission:create", "description": "create permission"},
29
+ {"name": "permission:read", "description": "read permission"},
30
+ {"name": "permission:update", "description": "update permission"},
31
+ {"name": "permission:delete", "description": "delete permission"},
32
+ # role
33
+ {"name": "role:create", "description": "create role"},
34
+ {"name": "role:read", "description": "read role"},
35
+ {"name": "role:update", "description": "update role"},
36
+ {"name": "role:delete", "description": "delete role"},
37
+ # user
38
+ {"name": "user:create", "description": "create user"},
39
+ {"name": "user:read", "description": "read user"},
40
+ {"name": "user:update", "description": "update user"},
41
+ {"name": "user:delete", "description": "delete user"},
42
+ ],
43
+ )
44
+ # ### end Alembic commands ###
45
+
46
+
47
+ def downgrade() -> None:
48
+ op.execute(
49
+ sa.delete(metadata.tables["permissions"]).where(
50
+ metadata.tables["permissions"].c.name.in_(
51
+ # user
52
+ "user:create",
53
+ "user:read",
54
+ "user:update",
55
+ "user:delete",
56
+ # role
57
+ "role:create",
58
+ "role:read",
59
+ "role:update",
60
+ "role:delete",
61
+ # permission
62
+ "permission:create",
63
+ "permission:read",
64
+ "permission:update",
65
+ "permission:delete",
66
+ )
67
+ )
68
+ )
69
+ # ### end Alembic commands ###
@@ -49,7 +49,7 @@ class UserDBRepository(
49
49
 
50
50
  def _select(self) -> Select:
51
51
  return (
52
- select(User, Role, Permission, UserSession)
52
+ select(User, Role, Permission)
53
53
  .join(UserRole, UserRole.user_id == User.id, isouter=True)
54
54
  .join(Role, Role.id == UserRole.role_id, isouter=True)
55
55
  .join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
@@ -62,7 +62,7 @@ class UserDBRepository(
62
62
  user_map: dict[str, dict[str, Any]] = {}
63
63
  user_role_map: dict[str, list[str]] = {}
64
64
  user_permission_map: dict[str, list[str]] = {}
65
- for user, role, permission, _ in rows:
65
+ for user, role, permission in rows:
66
66
  if user.id not in user_map:
67
67
  user_map[user.id] = {"user": user, "roles": [], "permissions": []}
68
68
  user_role_map[user.id] = []
@@ -241,6 +241,9 @@ class UserDBRepository(
241
241
  return UserSessionResponse(
242
242
  id=user_session.id,
243
243
  user_id=user_session.user_id,
244
+ access_token=user_session.access_token,
244
245
  access_token_expired_at=user_session.access_token_expired_at,
246
+ refresh_token=user_session.refresh_token,
245
247
  refresh_token_expired_at=user_session.refresh_token_expired_at,
248
+ token_type="bearer",
246
249
  )
@@ -46,8 +46,20 @@ class UserService(BaseService):
46
46
  methods=["get"],
47
47
  response_model=AuthUserResponse,
48
48
  )
49
- async def get_current_user(self, access_token: str) -> AuthUserResponse:
50
- return self._get_auth_user_by_access_token(access_token)
49
+ async def get_current_user(self, access_token: str | None) -> AuthUserResponse:
50
+ if access_token is None or access_token == "":
51
+ return self._get_guest_user()
52
+ try:
53
+ user_session = await self.user_repository.get_user_session_by_access_token(
54
+ access_token
55
+ )
56
+ user_id = user_session.user_id
57
+ if user_id == self.config.super_user:
58
+ return self._get_super_user()
59
+ user = await self.user_repository.get_by_id(user_id)
60
+ return self._to_auth_user_response(user)
61
+ except NotFoundError:
62
+ return self._get_guest_user()
51
63
 
52
64
  @BaseService.route(
53
65
  "/api/v1/user-sessions",
@@ -78,7 +90,7 @@ class UserService(BaseService):
78
90
  async def update_user_session(self, refresh_token: str) -> UserSessionResponse:
79
91
  current_user = await self._get_auth_user_by_refresh_token(refresh_token)
80
92
  current_user_session = (
81
- await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
93
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
82
94
  )
83
95
  token_data = self._create_user_token_data(current_user.username)
84
96
  return await self.user_repository.update_user_session(
@@ -94,7 +106,7 @@ class UserService(BaseService):
94
106
  )
95
107
  async def delete_user_session(self, refresh_token: str) -> UserSessionResponse:
96
108
  current_user_session = (
97
- await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
109
+ await self.user_repository.get_user_session_by_refresh_token(refresh_token)
98
110
  )
99
111
  await self.user_repository.delete_user_sessions([current_user_session.id])
100
112
  return current_user_session
@@ -231,23 +243,6 @@ class UserService(BaseService):
231
243
  user = await self.user_repository.get_by_id(user_id)
232
244
  return self._to_auth_user_response(user)
233
245
 
234
- async def _get_auth_user_by_access_token(
235
- self, access_token: str
236
- ) -> AuthUserResponse:
237
- if access_token is None or access_token == "":
238
- return self._get_guest_user()
239
- user_session = await self.user_repository.get_user_session_by_access_token(
240
- access_token
241
- )
242
- user_id = user_session.user_id
243
- if user_id == self.config.super_user:
244
- return self._get_super_user()
245
- try:
246
- user = await self.user_repository.get_by_id(user_id)
247
- return self._to_auth_user_response(user)
248
- except NotFoundError:
249
- return self._get_guest_user()
250
-
251
246
  async def _get_user_by_credentials(
252
247
  self, credentials: UserCredentials
253
248
  ) -> AuthUserResponse:
@@ -0,0 +1,39 @@
1
+ from my_app_name.module.gateway.schema.navigation import Navigation, Page, PageGroup
2
+
3
+ APP_NAVIGATION = Navigation()
4
+
5
+ APP_NAVIGATION.append_page(Page(name="gateway.home", caption="Home", url="/"))
6
+
7
+ auth_menu = APP_NAVIGATION.append_page_group(
8
+ PageGroup(
9
+ name="auth",
10
+ caption="Authorization",
11
+ )
12
+ )
13
+
14
+ auth_menu.append_page(
15
+ Page(
16
+ name="auth.permission",
17
+ caption="Permission",
18
+ url="/auth/permissions",
19
+ permission="permission:read",
20
+ )
21
+ )
22
+
23
+ auth_menu.append_page(
24
+ Page(
25
+ name="auth.role",
26
+ caption="Role",
27
+ url="/auth/roles",
28
+ permission="role:read",
29
+ )
30
+ )
31
+
32
+ auth_menu.append_page(
33
+ Page(
34
+ name="auth.user",
35
+ caption="User",
36
+ url="/auth/users",
37
+ permission="user:read",
38
+ )
39
+ )
@@ -1,18 +1,21 @@
1
- import os
1
+ from typing import Annotated
2
2
 
3
- from fastapi import FastAPI, HTTPException, Request
3
+ from fastapi import Depends, FastAPI, HTTPException, Request
4
4
  from fastapi.exception_handlers import http_exception_handler
5
- from fastapi.responses import HTMLResponse
5
+ from fastapi.responses import HTMLResponse, RedirectResponse
6
6
  from my_app_name.common.app_factory import app
7
7
  from my_app_name.common.schema import BasicResponse
8
8
  from my_app_name.config import (
9
- APP_GATEWAY_VIEW_PATH,
9
+ APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
10
10
  APP_MAIN_MODULE,
11
11
  APP_MODE,
12
12
  APP_MODULES,
13
13
  )
14
+ from my_app_name.module.auth.client.auth_client_factory import auth_client
14
15
  from my_app_name.module.gateway.subroute.auth import serve_auth_route
15
- from my_app_name.module.gateway.util.view import render, render_error
16
+ from my_app_name.module.gateway.util.auth import get_current_user
17
+ from my_app_name.module.gateway.util.view import render_content, render_error
18
+ from my_app_name.schema.user import AuthUserResponse
16
19
 
17
20
 
18
21
  def serve_route(app: FastAPI):
@@ -21,18 +24,48 @@ def serve_route(app: FastAPI):
21
24
  if APP_MODE == "monolith" or APP_MAIN_MODULE == "gateway":
22
25
  _serve_health_check(app)
23
26
  _serve_readiness_check(app)
24
- _serve_homepage(app)
27
+ _serve_common_pages(app)
25
28
  _handle_404(app)
26
29
 
27
30
  # Serve auth routes
28
31
  serve_auth_route(app)
29
32
 
30
33
 
31
- def _serve_homepage(app: FastAPI):
34
+ def _serve_common_pages(app: FastAPI):
32
35
  @app.get("/", include_in_schema=False)
33
- def home_page():
34
- return render(
35
- view_path=os.path.join(APP_GATEWAY_VIEW_PATH, "content", "homepage.html")
36
+ def home_page(
37
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
38
+ ):
39
+ return render_content(
40
+ view_path="homepage.html",
41
+ current_user=current_user,
42
+ page_name="gateway.home",
43
+ )
44
+
45
+ @app.get("/login", include_in_schema=False)
46
+ def login_page(
47
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
48
+ ):
49
+ if not current_user.is_guest:
50
+ return RedirectResponse("/")
51
+ return render_content(
52
+ view_path="login.html",
53
+ current_user=current_user,
54
+ page_name="gateway.home",
55
+ partials={"show_user_info": False},
56
+ )
57
+
58
+ @app.get("/logout", include_in_schema=False)
59
+ def logout_page(
60
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
61
+ ):
62
+ if current_user is None:
63
+ return RedirectResponse("/")
64
+ return render_content(
65
+ view_path="logout.html",
66
+ current_user=current_user,
67
+ page_name="gateway.home",
68
+ partials={"show_user_info": False},
36
69
  )
37
70
 
38
71
 
@@ -60,7 +93,15 @@ def _handle_404(app: FastAPI):
60
93
  if request.url.path.startswith("/api"):
61
94
  # Re-raise the exception to let FastAPI handle it
62
95
  return await http_exception_handler(request, exc)
63
- return render_error(error_message="Not found", status_code=404)
96
+ # Get current user by cookies
97
+ current_user = None
98
+ cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
99
+ if cookie_access_token is not None and cookie_access_token != "":
100
+ current_user = await auth_client.get_current_user(cookie_access_token)
101
+ # Show error page
102
+ return render_error(
103
+ error_message="Not found", status_code=404, current_user=current_user
104
+ )
64
105
 
65
106
 
66
107
  serve_route(app)
@@ -0,0 +1,95 @@
1
+ from my_app_name.schema.user import AuthUserResponse
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Page(BaseModel):
6
+ name: str
7
+ caption: str
8
+ url: str
9
+ permission: str | None = None
10
+
11
+
12
+ class AccessiblePage(BaseModel):
13
+ name: str
14
+ caption: str
15
+ url: str
16
+ active: bool
17
+
18
+
19
+ class PageGroup(BaseModel):
20
+ name: str
21
+ caption: str
22
+ pages: list[Page] = []
23
+
24
+ def append_page(self, submenu: Page) -> Page:
25
+ self.pages.append(submenu)
26
+ return submenu
27
+
28
+ def get_accessible_pages(
29
+ self, submenu_name: str | None = None, user: AuthUserResponse | None = None
30
+ ) -> list[AccessiblePage]:
31
+ return [
32
+ AccessiblePage(
33
+ name=page.name,
34
+ caption=page.caption,
35
+ url=page.url,
36
+ active=page.name == submenu_name,
37
+ )
38
+ for page in self.pages
39
+ if _has_permission(user, page.permission)
40
+ ]
41
+
42
+
43
+ class AccessiblePageGroup(BaseModel):
44
+ name: str
45
+ caption: str
46
+ pages: list[AccessiblePage]
47
+ active: bool
48
+
49
+
50
+ class Navigation(BaseModel):
51
+ items: list[PageGroup | Page] = []
52
+
53
+ def append_page_group(self, page_group: PageGroup) -> PageGroup:
54
+ self.items.append(page_group)
55
+ return page_group
56
+
57
+ def append_page(self, page: Page) -> Page:
58
+ self.items.append(page)
59
+ return page
60
+
61
+ def get_accessible_items(
62
+ self, page_name: str | None, user: AuthUserResponse | None
63
+ ) -> list[AccessiblePageGroup | AccessiblePage]:
64
+ accessible_items = []
65
+ for item in self.items:
66
+ if isinstance(item, Page) and _has_permission(user, item.permission):
67
+ accessible_items.append(
68
+ AccessiblePage(
69
+ name=item.name,
70
+ caption=item.caption,
71
+ url=item.url,
72
+ active=item.name == page_name,
73
+ )
74
+ )
75
+ continue
76
+ accessible_submenus = item.get_accessible_pages(page_name, user)
77
+ if accessible_submenus:
78
+ active = any(submenu.active for submenu in accessible_submenus)
79
+ accessible_items.append(
80
+ AccessiblePageGroup(
81
+ name=item.name,
82
+ caption=item.caption,
83
+ pages=accessible_submenus,
84
+ active=active,
85
+ )
86
+ )
87
+ return accessible_items
88
+
89
+
90
+ def _has_permission(user: AuthUserResponse | None, permission: str | None):
91
+ if permission is None:
92
+ return True
93
+ if user is not None:
94
+ return user.has_permission(permission)
95
+ return False