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
@@ -1,8 +1,17 @@
1
+ import os
1
2
  from typing import Annotated
2
3
 
3
- from fastapi import Depends, FastAPI, Response
4
+ from fastapi import Depends, FastAPI, Request, Response
4
5
  from fastapi.security import OAuth2PasswordRequestForm
6
+ from my_app_name.common.error import ForbiddenError, NotFoundError
5
7
  from my_app_name.module.auth.client.auth_client_factory import auth_client
8
+ from my_app_name.module.gateway.util.auth import (
9
+ get_current_user,
10
+ get_refresh_token,
11
+ set_user_session_cookie,
12
+ unset_user_session_cookie,
13
+ )
14
+ from my_app_name.module.gateway.util.view import render_content, render_error
6
15
  from my_app_name.schema.permission import (
7
16
  MultiplePermissionResponse,
8
17
  PermissionCreate,
@@ -16,6 +25,7 @@ from my_app_name.schema.role import (
16
25
  RoleUpdateWithPermissions,
17
26
  )
18
27
  from my_app_name.schema.user import (
28
+ AuthUserResponse,
19
29
  MultipleUserResponse,
20
30
  UserCreateWithRoles,
21
31
  UserCredentials,
@@ -37,203 +47,426 @@ def serve_auth_route(app: FastAPI):
37
47
  password=form_data.password,
38
48
  )
39
49
  )
50
+ set_user_session_cookie(response, user_session)
40
51
  return user_session
41
52
 
42
53
  @app.put("/api/v1/user-sessions", response_model=UserSessionResponse)
43
- async def update_user_session(refresh_token: str) -> UserSessionResponse:
44
- return await auth_client.update_user_session(refresh_token)
54
+ async def update_user_session(
55
+ request: Request, response: Response, refresh_token: str | None = None
56
+ ) -> UserSessionResponse:
57
+ actual_refresh_token = get_refresh_token(request, refresh_token)
58
+ if actual_refresh_token is None:
59
+ raise ForbiddenError("Refresh token needed")
60
+ try:
61
+ user_session = await auth_client.update_user_session(actual_refresh_token)
62
+ except NotFoundError:
63
+ raise ForbiddenError("Session not found")
64
+ set_user_session_cookie(response, user_session)
65
+ return user_session
45
66
 
46
67
  @app.delete("/api/v1/user-sessions", response_model=UserSessionResponse)
47
- async def delete_user_session(refresh_token: str) -> UserSessionResponse:
48
- return await auth_client.delete_user_session(refresh_token)
68
+ async def delete_user_session(
69
+ request: Request, response: Response, refresh_token: str | None = None
70
+ ) -> UserSessionResponse:
71
+ try:
72
+ actual_refresh_token = get_refresh_token(request, refresh_token)
73
+ if actual_refresh_token is None:
74
+ raise ForbiddenError("Refresh token needed")
75
+ user_session = await auth_client.delete_user_session(actual_refresh_token)
76
+ return user_session
77
+ finally:
78
+ unset_user_session_cookie(response)
49
79
 
50
80
  # Permission routes
51
81
 
82
+ @app.get("/auth/permissions", include_in_schema=False)
83
+ def permissions_crud_ui(
84
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
85
+ page: int = 1,
86
+ page_size: int = 10,
87
+ sort: str | None = None,
88
+ filter: str | None = None,
89
+ ):
90
+ if not current_user.has_permission("permission:read"):
91
+ return render_error(error_message="Access denied", status_code=403)
92
+ return render_content(
93
+ view_path=os.path.join("auth", "permission.html"),
94
+ current_user=current_user,
95
+ page_name="auth.permission",
96
+ page=page,
97
+ page_size=page_size,
98
+ sort=sort,
99
+ filter=filter,
100
+ allow_create=current_user.has_permission("permission:create"),
101
+ allow_update=current_user.has_permission("permission:update"),
102
+ allow_delete=current_user.has_permission("permission:delete"),
103
+ )
104
+
52
105
  @app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
53
106
  async def get_permissions(
107
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
54
108
  page: int = 1,
55
109
  page_size: int = 10,
56
110
  sort: str | None = None,
57
111
  filter: str | None = None,
58
112
  ) -> MultiplePermissionResponse:
113
+ if not current_user.has_permission("permission:read"):
114
+ raise ForbiddenError("Access denied")
59
115
  return await auth_client.get_permissions(
60
116
  page=page, page_size=page_size, sort=sort, filter=filter
61
117
  )
62
118
 
63
119
  @app.get("/api/v1/permissions/{permission_id}", response_model=PermissionResponse)
64
- async def get_permission_by_id(permission_id: str) -> PermissionResponse:
120
+ async def get_permission_by_id(
121
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
122
+ permission_id: str,
123
+ ) -> PermissionResponse:
124
+ if not current_user.has_permission("permission:read"):
125
+ raise ForbiddenError("Access denied")
65
126
  return await auth_client.get_permission_by_id(permission_id)
66
127
 
67
128
  @app.post(
68
129
  "/api/v1/permissions/bulk",
69
130
  response_model=list[PermissionResponse],
70
131
  )
71
- async def create_permission_bulk(data: list[PermissionCreate]):
132
+ async def create_permission_bulk(
133
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
134
+ data: list[PermissionCreate],
135
+ ) -> list[PermissionResponse]:
136
+ if not current_user.has_permission("permission:create"):
137
+ raise ForbiddenError("Access denied")
72
138
  return await auth_client.create_permission_bulk(
73
- [row.with_audit(created_by="system") for row in data]
139
+ [row.with_audit(created_by=current_user.id) for row in data]
74
140
  )
75
141
 
76
142
  @app.post(
77
143
  "/api/v1/permissions",
78
144
  response_model=PermissionResponse,
79
145
  )
80
- async def create_permission(data: PermissionCreate):
81
- return await auth_client.create_permission(data.with_audit(created_by="system"))
146
+ async def create_permission(
147
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
148
+ data: PermissionCreate,
149
+ ) -> PermissionResponse:
150
+ if not current_user.has_permission("permission:create"):
151
+ raise ForbiddenError("Access denied")
152
+ return await auth_client.create_permission(
153
+ data.with_audit(created_by=current_user.id)
154
+ )
82
155
 
83
156
  @app.put(
84
157
  "/api/v1/permissions/bulk",
85
158
  response_model=list[PermissionResponse],
86
159
  )
87
- async def update_permission_bulk(permission_ids: list[str], data: PermissionUpdate):
160
+ async def update_permission_bulk(
161
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
162
+ permission_ids: list[str],
163
+ data: PermissionUpdate,
164
+ ) -> list[PermissionResponse]:
165
+ if not current_user.has_permission("permission:update"):
166
+ raise ForbiddenError("Access denied")
88
167
  return await auth_client.update_permission_bulk(
89
- permission_ids, data.with_audit(updated_by="system")
168
+ permission_ids, data.with_audit(updated_by=current_user.id)
90
169
  )
91
170
 
92
171
  @app.put(
93
172
  "/api/v1/permissions/{permission_id}",
94
173
  response_model=PermissionResponse,
95
174
  )
96
- async def update_permission(permission_id: str, data: PermissionUpdate):
97
- return await auth_client.update_permission(data.with_audit(updated_by="system"))
175
+ async def update_permission(
176
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
177
+ permission_id: str,
178
+ data: PermissionUpdate,
179
+ ) -> PermissionResponse:
180
+ if not current_user.has_permission("permission:update"):
181
+ raise ForbiddenError("Access denied")
182
+ return await auth_client.update_permission(
183
+ permission_id, data.with_audit(updated_by=current_user.id)
184
+ )
98
185
 
99
186
  @app.delete(
100
187
  "/api/v1/permissions/bulk",
101
188
  response_model=list[PermissionResponse],
102
189
  )
103
- async def delete_permission_bulk(permission_ids: list[str]):
190
+ async def delete_permission_bulk(
191
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
192
+ permission_ids: list[str],
193
+ ) -> list[PermissionResponse]:
194
+ if not current_user.has_permission("permission:delete"):
195
+ raise ForbiddenError("Access denied")
104
196
  return await auth_client.delete_permission_bulk(
105
- permission_ids, deleted_by="system"
197
+ permission_ids, deleted_by=current_user.id
106
198
  )
107
199
 
108
200
  @app.delete(
109
201
  "/api/v1/permissions/{permission_id}",
110
202
  response_model=PermissionResponse,
111
203
  )
112
- async def delete_permission(permission_id: str):
113
- return await auth_client.delete_permission(permission_id, deleted_by="system")
204
+ async def delete_permission(
205
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
206
+ permission_id: str,
207
+ ) -> PermissionResponse:
208
+ if not current_user.has_permission("permission:delete"):
209
+ raise ForbiddenError("Access denied")
210
+ return await auth_client.delete_permission(
211
+ permission_id, deleted_by=current_user.id
212
+ )
114
213
 
115
214
  # Role routes
116
215
 
216
+ @app.get("/auth/roles", include_in_schema=False)
217
+ def roles_crud_ui(
218
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
219
+ page: int = 1,
220
+ page_size: int = 10,
221
+ sort: str | None = None,
222
+ filter: str | None = None,
223
+ ):
224
+ if not current_user.has_permission("role:read"):
225
+ return render_error(error_message="Access denied", status_code=403)
226
+ return render_content(
227
+ view_path=os.path.join("auth", "role.html"),
228
+ current_user=current_user,
229
+ page_name="auth.role",
230
+ page=page,
231
+ page_size=page_size,
232
+ sort=sort,
233
+ filter=filter,
234
+ allow_create=current_user.has_permission("role:create"),
235
+ allow_update=current_user.has_permission("role:update"),
236
+ allow_delete=current_user.has_permission("role:delete"),
237
+ )
238
+
117
239
  @app.get("/api/v1/roles", response_model=MultipleRoleResponse)
118
240
  async def get_roles(
241
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
119
242
  page: int = 1,
120
243
  page_size: int = 10,
121
244
  sort: str | None = None,
122
245
  filter: str | None = None,
123
246
  ) -> MultipleRoleResponse:
247
+ if not current_user.has_permission("role:read"):
248
+ raise ForbiddenError("Access denied")
124
249
  return await auth_client.get_roles(
125
250
  page=page, page_size=page_size, sort=sort, filter=filter
126
251
  )
127
252
 
128
253
  @app.get("/api/v1/roles/{role_id}", response_model=RoleResponse)
129
- async def get_role_by_id(role_id: str) -> RoleResponse:
254
+ async def get_role_by_id(
255
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
256
+ role_id: str,
257
+ ) -> RoleResponse:
258
+ if not current_user.has_permission("role:read"):
259
+ raise ForbiddenError("Access denied")
130
260
  return await auth_client.get_role_by_id(role_id)
131
261
 
132
262
  @app.post(
133
263
  "/api/v1/roles/bulk",
134
264
  response_model=list[RoleResponse],
135
265
  )
136
- async def create_role_bulk(data: list[RoleCreateWithPermissions]):
266
+ async def create_role_bulk(
267
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
268
+ data: list[RoleCreateWithPermissions],
269
+ ) -> list[RoleResponse]:
270
+ if not current_user.has_permission("role:create"):
271
+ raise ForbiddenError("Access denied")
137
272
  return await auth_client.create_role_bulk(
138
- [row.with_audit(created_by="system") for row in data]
273
+ [row.with_audit(created_by=current_user.id) for row in data]
139
274
  )
140
275
 
141
276
  @app.post(
142
277
  "/api/v1/roles",
143
278
  response_model=RoleResponse,
144
279
  )
145
- async def create_role(data: RoleCreateWithPermissions):
146
- return await auth_client.create_role(data.with_audit(created_by="system"))
280
+ async def create_role(
281
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
282
+ data: RoleCreateWithPermissions,
283
+ ) -> RoleResponse:
284
+ if not current_user.has_permission("role:create"):
285
+ raise ForbiddenError("Access denied")
286
+ return await auth_client.create_role(
287
+ data.with_audit(created_by=current_user.id)
288
+ )
147
289
 
148
290
  @app.put(
149
291
  "/api/v1/roles/bulk",
150
292
  response_model=list[RoleResponse],
151
293
  )
152
- async def update_role_bulk(role_ids: list[str], data: RoleUpdateWithPermissions):
294
+ async def update_role_bulk(
295
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
296
+ role_ids: list[str],
297
+ data: RoleUpdateWithPermissions,
298
+ ) -> list[RoleResponse]:
299
+ if not current_user.has_permission("role:update"):
300
+ raise ForbiddenError("Access denied")
153
301
  return await auth_client.update_role_bulk(
154
- role_ids, data.with_audit(updated_by="system")
302
+ role_ids, data.with_audit(updated_by=current_user.id)
155
303
  )
156
304
 
157
305
  @app.put(
158
306
  "/api/v1/roles/{role_id}",
159
307
  response_model=RoleResponse,
160
308
  )
161
- async def update_role(role_id: str, data: RoleUpdateWithPermissions):
162
- return await auth_client.update_role(data.with_audit(updated_by="system"))
309
+ async def update_role(
310
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
311
+ role_id: str,
312
+ data: RoleUpdateWithPermissions,
313
+ ) -> RoleResponse:
314
+ if not current_user.has_permission("role:update"):
315
+ raise ForbiddenError("Access denied")
316
+ return await auth_client.update_role(
317
+ role_id, data.with_audit(updated_by=current_user.id)
318
+ )
163
319
 
164
320
  @app.delete(
165
321
  "/api/v1/roles/bulk",
166
322
  response_model=list[RoleResponse],
167
323
  )
168
- async def delete_role_bulk(role_ids: list[str]):
169
- return await auth_client.delete_role_bulk(role_ids, deleted_by="system")
324
+ async def delete_role_bulk(
325
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
326
+ role_ids: list[str],
327
+ ) -> list[RoleResponse]:
328
+ if not current_user.has_permission("role:delete"):
329
+ raise ForbiddenError("Access denied")
330
+ return await auth_client.delete_role_bulk(role_ids, deleted_by=current_user.id)
170
331
 
171
332
  @app.delete(
172
333
  "/api/v1/roles/{role_id}",
173
334
  response_model=RoleResponse,
174
335
  )
175
- async def delete_role(role_id: str):
176
- return await auth_client.delete_role(role_id, deleted_by="system")
336
+ async def delete_role(
337
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
338
+ role_id: str,
339
+ ) -> RoleResponse:
340
+ if not current_user.has_permission("role:delete"):
341
+ raise ForbiddenError("Access denied")
342
+ return await auth_client.delete_role(role_id, deleted_by=current_user.id)
177
343
 
178
344
  # User routes
179
345
 
346
+ @app.get("/auth/users", include_in_schema=False)
347
+ def users_crud_ui(
348
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
349
+ page: int = 1,
350
+ page_size: int = 10,
351
+ sort: str | None = None,
352
+ filter: str | None = None,
353
+ ):
354
+ if not current_user.has_permission("user:read"):
355
+ return render_error(error_message="Access denied", status_code=403)
356
+ return render_content(
357
+ view_path=os.path.join("auth", "user.html"),
358
+ current_user=current_user,
359
+ page_name="auth.user",
360
+ page=page,
361
+ page_size=page_size,
362
+ sort=sort,
363
+ filter=filter,
364
+ allow_create=current_user.has_permission("user:create"),
365
+ allow_update=current_user.has_permission("user:update"),
366
+ allow_delete=current_user.has_permission("user:delete"),
367
+ )
368
+
180
369
  @app.get("/api/v1/users", response_model=MultipleUserResponse)
181
370
  async def get_users(
371
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
182
372
  page: int = 1,
183
373
  page_size: int = 10,
184
374
  sort: str | None = None,
185
375
  filter: str | None = None,
186
376
  ) -> MultipleUserResponse:
377
+ if not current_user.has_permission("user:read"):
378
+ raise ForbiddenError("Access denied")
187
379
  return await auth_client.get_users(
188
380
  page=page, page_size=page_size, sort=sort, filter=filter
189
381
  )
190
382
 
191
383
  @app.get("/api/v1/users/{user_id}", response_model=UserResponse)
192
- async def get_user_by_id(user_id: str) -> UserResponse:
384
+ async def get_user_by_id(
385
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
386
+ user_id: str,
387
+ ) -> UserResponse:
388
+ if not current_user.has_permission("user:read"):
389
+ raise ForbiddenError("Access denied")
193
390
  return await auth_client.get_user_by_id(user_id)
194
391
 
195
392
  @app.post(
196
393
  "/api/v1/users/bulk",
197
394
  response_model=list[UserResponse],
198
395
  )
199
- async def create_user_bulk(data: list[UserCreateWithRoles]):
396
+ async def create_user_bulk(
397
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
398
+ data: list[UserCreateWithRoles],
399
+ ) -> list[UserResponse]:
400
+ if not current_user.has_permission("user:create"):
401
+ raise ForbiddenError("Access denied")
200
402
  return await auth_client.create_user_bulk(
201
- [row.with_audit(created_by="system") for row in data]
403
+ [row.with_audit(created_by=current_user.id) for row in data]
202
404
  )
203
405
 
204
406
  @app.post(
205
407
  "/api/v1/users",
206
408
  response_model=UserResponse,
207
409
  )
208
- async def create_user(data: UserCreateWithRoles):
209
- return await auth_client.create_user(data.with_audit(created_by="system"))
410
+ async def create_user(
411
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
412
+ data: UserCreateWithRoles,
413
+ ) -> UserResponse:
414
+ if not current_user.has_permission("user:create"):
415
+ raise ForbiddenError("Access denied")
416
+ return await auth_client.create_user(
417
+ data.with_audit(created_by=current_user.id)
418
+ )
210
419
 
211
420
  @app.put(
212
421
  "/api/v1/users/bulk",
213
422
  response_model=list[UserResponse],
214
423
  )
215
- async def update_user_bulk(user_ids: list[str], data: UserUpdateWithRoles):
424
+ async def update_user_bulk(
425
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
426
+ user_ids: list[str],
427
+ data: UserUpdateWithRoles,
428
+ ) -> list[UserResponse]:
429
+ if not current_user.has_permission("user:update"):
430
+ raise ForbiddenError("Access denied")
216
431
  return await auth_client.update_user_bulk(
217
- user_ids, data.with_audit(updated_by="system")
432
+ user_ids, data.with_audit(updated_by=current_user.id)
218
433
  )
219
434
 
220
435
  @app.put(
221
436
  "/api/v1/users/{user_id}",
222
437
  response_model=UserResponse,
223
438
  )
224
- async def update_user(user_id: str, data: UserUpdateWithRoles):
225
- return await auth_client.update_user(data.with_audit(updated_by="system"))
439
+ async def update_user(
440
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
441
+ user_id: str,
442
+ data: UserUpdateWithRoles,
443
+ ) -> UserResponse:
444
+ if not current_user.has_permission("user:update"):
445
+ raise ForbiddenError("Access denied")
446
+ return await auth_client.update_user(
447
+ user_id, data.with_audit(updated_by=current_user.id)
448
+ )
226
449
 
227
450
  @app.delete(
228
451
  "/api/v1/users/bulk",
229
452
  response_model=list[UserResponse],
230
453
  )
231
- async def delete_user_bulk(user_ids: list[str]):
232
- return await auth_client.delete_user_bulk(user_ids, deleted_by="system")
454
+ async def delete_user_bulk(
455
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
456
+ user_ids: list[str],
457
+ ) -> list[UserResponse]:
458
+ if not current_user.has_permission("user:delete"):
459
+ raise ForbiddenError("Access denied")
460
+ return await auth_client.delete_user_bulk(user_ids, deleted_by=current_user.id)
233
461
 
234
462
  @app.delete(
235
463
  "/api/v1/users/{user_id}",
236
464
  response_model=UserResponse,
237
465
  )
238
- async def delete_user(user_id: str):
239
- return await auth_client.delete_user(user_id, deleted_by="system")
466
+ async def delete_user(
467
+ current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
468
+ user_id: str,
469
+ ) -> UserResponse:
470
+ if not current_user.has_permission("user:delete"):
471
+ raise ForbiddenError("Access denied")
472
+ return await auth_client.delete_user(user_id, deleted_by=current_user.id)
@@ -0,0 +1,66 @@
1
+ import datetime
2
+
3
+ from fastapi import Depends, Request, Response
4
+ from fastapi.security import OAuth2PasswordBearer
5
+ from my_app_name.config import (
6
+ APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
7
+ APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
8
+ APP_AUTH_REFRESH_TOKEN_COOKIE_NAME,
9
+ APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
10
+ )
11
+ from my_app_name.module.auth.client.auth_client_factory import auth_client
12
+ from my_app_name.schema.user import AuthUserResponse, UserSessionResponse
13
+ from typing_extensions import Annotated
14
+
15
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/user-sessions", auto_error=False)
16
+
17
+
18
+ def get_refresh_token(request: Request, refresh_token: str | None) -> str | None:
19
+ if refresh_token is not None and refresh_token != "":
20
+ return refresh_token
21
+ cookie_refresh_token = request.cookies.get(APP_AUTH_REFRESH_TOKEN_COOKIE_NAME)
22
+ if cookie_refresh_token is not None and cookie_refresh_token != "":
23
+ return cookie_refresh_token
24
+ return None
25
+
26
+
27
+ async def get_current_user(
28
+ request: Request,
29
+ response: Response,
30
+ bearer_access_token: Annotated[str, Depends(oauth2_scheme)],
31
+ ) -> AuthUserResponse:
32
+ # Bearer token exists
33
+ if bearer_access_token is not None and bearer_access_token != "":
34
+ return await auth_client.get_current_user(bearer_access_token)
35
+ cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
36
+ # Cookie exists
37
+ if cookie_access_token is not None and cookie_access_token != "":
38
+ cookie_user = await auth_client.get_current_user(cookie_access_token)
39
+ if cookie_user.is_guest:
40
+ # If user is guest, the cookie is not needed
41
+ unset_user_session_cookie(response)
42
+ return cookie_user
43
+ # No bearer token or cookie
44
+ return await auth_client.get_current_user("")
45
+
46
+
47
+ def set_user_session_cookie(response: Response, user_session: UserSessionResponse):
48
+ response.set_cookie(
49
+ key=APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
50
+ value=user_session.access_token,
51
+ httponly=True,
52
+ max_age=60 * APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
53
+ expires=user_session.access_token_expired_at.astimezone(datetime.timezone.utc),
54
+ )
55
+ response.set_cookie(
56
+ key=APP_AUTH_REFRESH_TOKEN_COOKIE_NAME,
57
+ value=user_session.refresh_token,
58
+ httponly=True,
59
+ max_age=60 * APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
60
+ expires=user_session.refresh_token_expired_at.astimezone(datetime.timezone.utc),
61
+ )
62
+
63
+
64
+ def unset_user_session_cookie(response: Response):
65
+ response.delete_cookie(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
66
+ response.delete_cookie(APP_AUTH_REFRESH_TOKEN_COOKIE_NAME)
@@ -1,18 +1,24 @@
1
1
  import os
2
2
  from typing import Any
3
3
 
4
+ import my_app_name.config as CFG
4
5
  from fastapi.responses import HTMLResponse
5
6
  from my_app_name.common.util.view import render_page, render_str
6
7
  from my_app_name.config import (
8
+ APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
7
9
  APP_GATEWAY_CSS_PATH_LIST,
8
10
  APP_GATEWAY_FAVICON_PATH,
11
+ APP_GATEWAY_FOOTER,
9
12
  APP_GATEWAY_JS_PATH_LIST,
10
13
  APP_GATEWAY_LOGO_PATH,
14
+ APP_GATEWAY_PICO_CSS_PATH,
11
15
  APP_GATEWAY_SUBTITLE,
12
16
  APP_GATEWAY_TITLE,
13
17
  APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH,
14
18
  APP_GATEWAY_VIEW_PATH,
15
19
  )
20
+ from my_app_name.module.gateway.config.navigation import APP_NAVIGATION
21
+ from my_app_name.schema.user import AuthUserResponse
16
22
 
17
23
  _DEFAULT_TEMPLATE_PATH = os.path.join(
18
24
  APP_GATEWAY_VIEW_PATH, APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH
@@ -24,32 +30,47 @@ _DEFAULT_ERROR_TEMPLATE_PATH = os.path.join(
24
30
  _DEFAULT_PARTIALS = {
25
31
  "title": APP_GATEWAY_TITLE,
26
32
  "subtitle": APP_GATEWAY_SUBTITLE,
33
+ "footer": APP_GATEWAY_FOOTER,
27
34
  "logo_path": APP_GATEWAY_LOGO_PATH,
28
35
  "favicon_path": APP_GATEWAY_FAVICON_PATH,
36
+ "pico_css_path": APP_GATEWAY_PICO_CSS_PATH,
29
37
  "css_path_list": APP_GATEWAY_CSS_PATH_LIST,
30
38
  "js_path_list": APP_GATEWAY_JS_PATH_LIST,
39
+ "show_user_info": True,
40
+ "should_refresh_session": True,
41
+ "refresh_session_interval_seconds": f"{APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES * 60 / 3}",
31
42
  }
32
43
 
33
44
 
34
- def render(
45
+ def render_content(
35
46
  view_path: str,
36
47
  template_path: str = _DEFAULT_TEMPLATE_PATH,
37
48
  status_code: int = 200,
38
- headers: dict[str, str] = None,
49
+ headers: dict[str, str] | None = None,
39
50
  media_type: str | None = None,
51
+ current_user: AuthUserResponse | None = None,
52
+ page_name: str | None = None,
40
53
  partials: dict[str, Any] = {},
41
- **data: Any
54
+ **data: Any,
42
55
  ) -> HTMLResponse:
43
56
  rendered_partials = {key: val for key, val in _DEFAULT_PARTIALS.items()}
44
57
  for key, val in partials.items():
45
58
  rendered_partials[key] = val
59
+ rendered_partials["page_name"] = page_name
60
+ rendered_partials["current_user"] = current_user
61
+ rendered_partials["navigations"] = APP_NAVIGATION.get_accessible_items(
62
+ page_name, current_user
63
+ )
46
64
  return render_page(
47
65
  template_path=template_path,
48
66
  status_code=status_code,
49
67
  headers=headers,
50
68
  media_type=media_type,
51
- partials=rendered_partials,
52
- content=render_str(template_path=view_path, **data),
69
+ content=render_str(
70
+ template_path=os.path.join(APP_GATEWAY_VIEW_PATH, "content", view_path),
71
+ **data,
72
+ ),
73
+ **rendered_partials,
53
74
  )
54
75
 
55
76
 
@@ -58,17 +79,21 @@ def render_error(
58
79
  status_code: int = 500,
59
80
  view_path: str = _DEFAULT_ERROR_TEMPLATE_PATH,
60
81
  template_path: str = _DEFAULT_TEMPLATE_PATH,
61
- headers: dict[str, str] = None,
82
+ headers: dict[str, str] | None = None,
62
83
  media_type: str | None = None,
84
+ current_user: AuthUserResponse | None = None,
85
+ page_name: str | None = None,
63
86
  partials: dict[str, Any] = {},
64
87
  ):
65
- return render(
88
+ return render_content(
66
89
  view_path=view_path,
67
90
  template_path=template_path,
68
91
  status_code=status_code,
69
92
  headers=headers,
70
93
  media_type=media_type,
71
- partials=partials,
94
+ current_user=current_user,
95
+ page_name=page_name,
96
+ partials={"show_user_info": False, **partials},
72
97
  error_status_code=status_code,
73
98
  error_message=error_message,
74
99
  )