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.
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.coveragerc +11 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/config.py +5 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +131 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +128 -5
- 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
- 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
- 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
- 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
- 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
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +81 -13
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +42 -3
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +8 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +56 -12
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +3 -3
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +19 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +277 -44
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +66 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +6 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +15 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
- zrb/runner/web_route/refresh_token_api_route.py +1 -1
- zrb/runner/web_route/static/refresh-token.template.js +9 -0
- zrb/runner/web_route/static/static_route.py +1 -1
- zrb/task/base_task.py +10 -10
- zrb/util/codemod/modification_mode.py +3 -0
- zrb/util/codemod/modify_class.py +58 -0
- zrb/util/codemod/modify_class_parent.py +68 -0
- zrb/util/codemod/modify_class_property.py +128 -0
- zrb/util/codemod/modify_dict.py +75 -0
- zrb/util/codemod/modify_function.py +65 -0
- zrb/util/codemod/modify_function_call.py +68 -0
- zrb/util/codemod/modify_method.py +88 -0
- zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
- zrb/util/file.py +3 -2
- zrb/util/load.py +13 -7
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/RECORD +80 -46
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
- zrb/util/codemod/append_code_to_class.py +0 -35
- zrb/util/codemod/append_code_to_function.py +0 -38
- zrb/util/codemod/append_code_to_method.py +0 -55
- zrb/util/codemod/append_key_to_dict.py +0 -51
- zrb/util/codemod/append_param_to_function_call.py +0 -39
- zrb/util/codemod/prepend_parent_to_class.py +0 -38
- zrb/util/codemod/prepend_property_to_class.py +0 -55
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py
CHANGED
@@ -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(
|
44
|
-
|
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(
|
48
|
-
|
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(
|
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(
|
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=
|
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(
|
81
|
-
|
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(
|
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=
|
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(
|
97
|
-
|
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(
|
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=
|
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(
|
113
|
-
|
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(
|
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(
|
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=
|
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(
|
146
|
-
|
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(
|
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=
|
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(
|
162
|
-
|
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(
|
169
|
-
|
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(
|
176
|
-
|
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(
|
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(
|
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=
|
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(
|
209
|
-
|
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(
|
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=
|
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(
|
225
|
-
|
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(
|
232
|
-
|
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(
|
239
|
-
|
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
|
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
|
-
|
52
|
-
|
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
|
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
|
-
|
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
|
)
|