zrb 1.0.0b9__py3-none-any.whl → 1.0.0b10__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 +4 -4
- 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 +107 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +67 -4
- 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 +57 -13
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +6 -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/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 +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +1 -0
- 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/subroute/auth.py +193 -43
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +57 -0
- 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 +17 -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/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-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/METADATA +1 -1
- {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.dist-info}/RECORD +53 -36
- 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.0.0b10.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b9.dist-info → zrb-1.0.0b10.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
|
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
|
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
|
-
|
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.
|
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.
|
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:
|
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py
CHANGED
@@ -2,7 +2,13 @@ from typing import Annotated
|
|
2
2
|
|
3
3
|
from fastapi import Depends, FastAPI, Response
|
4
4
|
from fastapi.security import OAuth2PasswordRequestForm
|
5
|
+
from my_app_name.common.error import ForbiddenError
|
5
6
|
from my_app_name.module.auth.client.auth_client_factory import auth_client
|
7
|
+
from my_app_name.module.gateway.util.auth import (
|
8
|
+
get_current_user,
|
9
|
+
set_user_session_cookie,
|
10
|
+
unset_user_session_cookie,
|
11
|
+
)
|
6
12
|
from my_app_name.schema.permission import (
|
7
13
|
MultiplePermissionResponse,
|
8
14
|
PermissionCreate,
|
@@ -16,6 +22,7 @@ from my_app_name.schema.role import (
|
|
16
22
|
RoleUpdateWithPermissions,
|
17
23
|
)
|
18
24
|
from my_app_name.schema.user import (
|
25
|
+
AuthUserResponse,
|
19
26
|
MultipleUserResponse,
|
20
27
|
UserCreateWithRoles,
|
21
28
|
UserCredentials,
|
@@ -37,203 +44,346 @@ def serve_auth_route(app: FastAPI):
|
|
37
44
|
password=form_data.password,
|
38
45
|
)
|
39
46
|
)
|
47
|
+
set_user_session_cookie(response, user_session)
|
40
48
|
return user_session
|
41
49
|
|
42
50
|
@app.put("/api/v1/user-sessions", response_model=UserSessionResponse)
|
43
|
-
async def update_user_session(
|
44
|
-
|
51
|
+
async def update_user_session(
|
52
|
+
response: Response, refresh_token: str
|
53
|
+
) -> UserSessionResponse:
|
54
|
+
user_session = await auth_client.update_user_session(refresh_token)
|
55
|
+
set_user_session_cookie(response, user_session)
|
56
|
+
return user_session
|
45
57
|
|
46
58
|
@app.delete("/api/v1/user-sessions", response_model=UserSessionResponse)
|
47
|
-
async def delete_user_session(
|
48
|
-
|
59
|
+
async def delete_user_session(
|
60
|
+
response: Response, refresh_token: str
|
61
|
+
) -> UserSessionResponse:
|
62
|
+
user_session = await auth_client.delete_user_session(refresh_token)
|
63
|
+
unset_user_session_cookie(response)
|
64
|
+
return user_session
|
49
65
|
|
50
66
|
# Permission routes
|
51
67
|
|
52
68
|
@app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
|
53
69
|
async def get_permissions(
|
70
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
54
71
|
page: int = 1,
|
55
72
|
page_size: int = 10,
|
56
73
|
sort: str | None = None,
|
57
74
|
filter: str | None = None,
|
58
75
|
) -> MultiplePermissionResponse:
|
76
|
+
if not current_user.has_permission("permission:read"):
|
77
|
+
raise ForbiddenError("Access denied")
|
59
78
|
return await auth_client.get_permissions(
|
60
79
|
page=page, page_size=page_size, sort=sort, filter=filter
|
61
80
|
)
|
62
81
|
|
63
82
|
@app.get("/api/v1/permissions/{permission_id}", response_model=PermissionResponse)
|
64
|
-
async def get_permission_by_id(
|
83
|
+
async def get_permission_by_id(
|
84
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
85
|
+
permission_id: str,
|
86
|
+
) -> PermissionResponse:
|
87
|
+
if not current_user.has_permission("permission:read"):
|
88
|
+
raise ForbiddenError("Access denied")
|
65
89
|
return await auth_client.get_permission_by_id(permission_id)
|
66
90
|
|
67
91
|
@app.post(
|
68
92
|
"/api/v1/permissions/bulk",
|
69
93
|
response_model=list[PermissionResponse],
|
70
94
|
)
|
71
|
-
async def create_permission_bulk(
|
95
|
+
async def create_permission_bulk(
|
96
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
97
|
+
data: list[PermissionCreate],
|
98
|
+
) -> list[PermissionResponse]:
|
99
|
+
if not current_user.has_permission("permission:create"):
|
100
|
+
raise ForbiddenError("Access denied")
|
72
101
|
return await auth_client.create_permission_bulk(
|
73
|
-
[row.with_audit(created_by=
|
102
|
+
[row.with_audit(created_by=current_user.id) for row in data]
|
74
103
|
)
|
75
104
|
|
76
105
|
@app.post(
|
77
106
|
"/api/v1/permissions",
|
78
107
|
response_model=PermissionResponse,
|
79
108
|
)
|
80
|
-
async def create_permission(
|
81
|
-
|
109
|
+
async def create_permission(
|
110
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
111
|
+
data: PermissionCreate,
|
112
|
+
) -> PermissionResponse:
|
113
|
+
if not current_user.has_permission("permission:create"):
|
114
|
+
raise ForbiddenError("Access denied")
|
115
|
+
return await auth_client.create_permission(
|
116
|
+
data.with_audit(created_by=current_user.id)
|
117
|
+
)
|
82
118
|
|
83
119
|
@app.put(
|
84
120
|
"/api/v1/permissions/bulk",
|
85
121
|
response_model=list[PermissionResponse],
|
86
122
|
)
|
87
|
-
async def update_permission_bulk(
|
123
|
+
async def update_permission_bulk(
|
124
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
125
|
+
permission_ids: list[str],
|
126
|
+
data: PermissionUpdate,
|
127
|
+
) -> list[PermissionResponse]:
|
128
|
+
if not current_user.has_permission("permission:update"):
|
129
|
+
raise ForbiddenError("Access denied")
|
88
130
|
return await auth_client.update_permission_bulk(
|
89
|
-
permission_ids, data.with_audit(updated_by=
|
131
|
+
permission_ids, data.with_audit(updated_by=current_user.id)
|
90
132
|
)
|
91
133
|
|
92
134
|
@app.put(
|
93
135
|
"/api/v1/permissions/{permission_id}",
|
94
136
|
response_model=PermissionResponse,
|
95
137
|
)
|
96
|
-
async def update_permission(
|
97
|
-
|
138
|
+
async def update_permission(
|
139
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
140
|
+
permission_id: str,
|
141
|
+
data: PermissionUpdate,
|
142
|
+
) -> PermissionResponse:
|
143
|
+
if not current_user.has_permission("permission:update"):
|
144
|
+
raise ForbiddenError("Access denied")
|
145
|
+
return await auth_client.update_permission(
|
146
|
+
permission_id, data.with_audit(updated_by=current_user.id)
|
147
|
+
)
|
98
148
|
|
99
149
|
@app.delete(
|
100
150
|
"/api/v1/permissions/bulk",
|
101
151
|
response_model=list[PermissionResponse],
|
102
152
|
)
|
103
|
-
async def delete_permission_bulk(
|
153
|
+
async def delete_permission_bulk(
|
154
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
155
|
+
permission_ids: list[str],
|
156
|
+
) -> list[PermissionResponse]:
|
157
|
+
if not current_user.has_permission("permission:delete"):
|
158
|
+
raise ForbiddenError("Access denied")
|
104
159
|
return await auth_client.delete_permission_bulk(
|
105
|
-
permission_ids, deleted_by=
|
160
|
+
permission_ids, deleted_by=current_user.id
|
106
161
|
)
|
107
162
|
|
108
163
|
@app.delete(
|
109
164
|
"/api/v1/permissions/{permission_id}",
|
110
165
|
response_model=PermissionResponse,
|
111
166
|
)
|
112
|
-
async def delete_permission(
|
113
|
-
|
167
|
+
async def delete_permission(
|
168
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
169
|
+
permission_id: str,
|
170
|
+
) -> PermissionResponse:
|
171
|
+
if not current_user.has_permission("permission:delete"):
|
172
|
+
raise ForbiddenError("Access denied")
|
173
|
+
return await auth_client.delete_permission(
|
174
|
+
permission_id, deleted_by=current_user.id
|
175
|
+
)
|
114
176
|
|
115
177
|
# Role routes
|
116
178
|
|
117
179
|
@app.get("/api/v1/roles", response_model=MultipleRoleResponse)
|
118
180
|
async def get_roles(
|
181
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
119
182
|
page: int = 1,
|
120
183
|
page_size: int = 10,
|
121
184
|
sort: str | None = None,
|
122
185
|
filter: str | None = None,
|
123
186
|
) -> MultipleRoleResponse:
|
187
|
+
if not current_user.has_permission("role:read"):
|
188
|
+
raise ForbiddenError("Access denied")
|
124
189
|
return await auth_client.get_roles(
|
125
190
|
page=page, page_size=page_size, sort=sort, filter=filter
|
126
191
|
)
|
127
192
|
|
128
193
|
@app.get("/api/v1/roles/{role_id}", response_model=RoleResponse)
|
129
|
-
async def get_role_by_id(
|
194
|
+
async def get_role_by_id(
|
195
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
196
|
+
role_id: str,
|
197
|
+
) -> RoleResponse:
|
198
|
+
if not current_user.has_permission("role:read"):
|
199
|
+
raise ForbiddenError("Access denied")
|
130
200
|
return await auth_client.get_role_by_id(role_id)
|
131
201
|
|
132
202
|
@app.post(
|
133
203
|
"/api/v1/roles/bulk",
|
134
204
|
response_model=list[RoleResponse],
|
135
205
|
)
|
136
|
-
async def create_role_bulk(
|
206
|
+
async def create_role_bulk(
|
207
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
208
|
+
data: list[RoleCreateWithPermissions],
|
209
|
+
) -> list[RoleResponse]:
|
210
|
+
if not current_user.has_permission("role:create"):
|
211
|
+
raise ForbiddenError("Access denied")
|
137
212
|
return await auth_client.create_role_bulk(
|
138
|
-
[row.with_audit(created_by=
|
213
|
+
[row.with_audit(created_by=current_user.id) for row in data]
|
139
214
|
)
|
140
215
|
|
141
216
|
@app.post(
|
142
217
|
"/api/v1/roles",
|
143
218
|
response_model=RoleResponse,
|
144
219
|
)
|
145
|
-
async def create_role(
|
146
|
-
|
220
|
+
async def create_role(
|
221
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
222
|
+
data: RoleCreateWithPermissions,
|
223
|
+
) -> RoleResponse:
|
224
|
+
if not current_user.has_permission("role:create"):
|
225
|
+
raise ForbiddenError("Access denied")
|
226
|
+
return await auth_client.create_role(
|
227
|
+
data.with_audit(created_by=current_user.id)
|
228
|
+
)
|
147
229
|
|
148
230
|
@app.put(
|
149
231
|
"/api/v1/roles/bulk",
|
150
232
|
response_model=list[RoleResponse],
|
151
233
|
)
|
152
|
-
async def update_role_bulk(
|
234
|
+
async def update_role_bulk(
|
235
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
236
|
+
role_ids: list[str],
|
237
|
+
data: RoleUpdateWithPermissions,
|
238
|
+
) -> list[RoleResponse]:
|
239
|
+
if not current_user.has_permission("role:update"):
|
240
|
+
raise ForbiddenError("Access denied")
|
153
241
|
return await auth_client.update_role_bulk(
|
154
|
-
role_ids, data.with_audit(updated_by=
|
242
|
+
role_ids, data.with_audit(updated_by=current_user.id)
|
155
243
|
)
|
156
244
|
|
157
245
|
@app.put(
|
158
246
|
"/api/v1/roles/{role_id}",
|
159
247
|
response_model=RoleResponse,
|
160
248
|
)
|
161
|
-
async def update_role(
|
162
|
-
|
249
|
+
async def update_role(
|
250
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
251
|
+
role_id: str,
|
252
|
+
data: RoleUpdateWithPermissions,
|
253
|
+
) -> RoleResponse:
|
254
|
+
if not current_user.has_permission("role:update"):
|
255
|
+
raise ForbiddenError("Access denied")
|
256
|
+
return await auth_client.update_role(
|
257
|
+
role_id, data.with_audit(updated_by=current_user.id)
|
258
|
+
)
|
163
259
|
|
164
260
|
@app.delete(
|
165
261
|
"/api/v1/roles/bulk",
|
166
262
|
response_model=list[RoleResponse],
|
167
263
|
)
|
168
|
-
async def delete_role_bulk(
|
169
|
-
|
264
|
+
async def delete_role_bulk(
|
265
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
266
|
+
role_ids: list[str],
|
267
|
+
) -> list[RoleResponse]:
|
268
|
+
if not current_user.has_permission("role:delete"):
|
269
|
+
raise ForbiddenError("Access denied")
|
270
|
+
return await auth_client.delete_role_bulk(role_ids, deleted_by=current_user.id)
|
170
271
|
|
171
272
|
@app.delete(
|
172
273
|
"/api/v1/roles/{role_id}",
|
173
274
|
response_model=RoleResponse,
|
174
275
|
)
|
175
|
-
async def delete_role(
|
176
|
-
|
276
|
+
async def delete_role(
|
277
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
278
|
+
role_id: str,
|
279
|
+
) -> RoleResponse:
|
280
|
+
if not current_user.has_permission("role:delete"):
|
281
|
+
raise ForbiddenError("Access denied")
|
282
|
+
return await auth_client.delete_role(role_id, deleted_by=current_user.id)
|
177
283
|
|
178
284
|
# User routes
|
179
285
|
|
180
286
|
@app.get("/api/v1/users", response_model=MultipleUserResponse)
|
181
287
|
async def get_users(
|
288
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
182
289
|
page: int = 1,
|
183
290
|
page_size: int = 10,
|
184
291
|
sort: str | None = None,
|
185
292
|
filter: str | None = None,
|
186
293
|
) -> MultipleUserResponse:
|
294
|
+
if not current_user.has_permission("user:read"):
|
295
|
+
raise ForbiddenError("Access denied")
|
187
296
|
return await auth_client.get_users(
|
188
297
|
page=page, page_size=page_size, sort=sort, filter=filter
|
189
298
|
)
|
190
299
|
|
191
300
|
@app.get("/api/v1/users/{user_id}", response_model=UserResponse)
|
192
|
-
async def get_user_by_id(
|
301
|
+
async def get_user_by_id(
|
302
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
303
|
+
user_id: str,
|
304
|
+
) -> UserResponse:
|
305
|
+
if not current_user.has_permission("user:read"):
|
306
|
+
raise ForbiddenError("Access denied")
|
193
307
|
return await auth_client.get_user_by_id(user_id)
|
194
308
|
|
195
309
|
@app.post(
|
196
310
|
"/api/v1/users/bulk",
|
197
311
|
response_model=list[UserResponse],
|
198
312
|
)
|
199
|
-
async def create_user_bulk(
|
313
|
+
async def create_user_bulk(
|
314
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
315
|
+
data: list[UserCreateWithRoles],
|
316
|
+
) -> list[UserResponse]:
|
317
|
+
if not current_user.has_permission("user:create"):
|
318
|
+
raise ForbiddenError("Access denied")
|
200
319
|
return await auth_client.create_user_bulk(
|
201
|
-
[row.with_audit(created_by=
|
320
|
+
[row.with_audit(created_by=current_user.id) for row in data]
|
202
321
|
)
|
203
322
|
|
204
323
|
@app.post(
|
205
324
|
"/api/v1/users",
|
206
325
|
response_model=UserResponse,
|
207
326
|
)
|
208
|
-
async def create_user(
|
209
|
-
|
327
|
+
async def create_user(
|
328
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
329
|
+
data: UserCreateWithRoles,
|
330
|
+
) -> UserResponse:
|
331
|
+
if not current_user.has_permission("user:create"):
|
332
|
+
raise ForbiddenError("Access denied")
|
333
|
+
return await auth_client.create_user(
|
334
|
+
data.with_audit(created_by=current_user.id)
|
335
|
+
)
|
210
336
|
|
211
337
|
@app.put(
|
212
338
|
"/api/v1/users/bulk",
|
213
339
|
response_model=list[UserResponse],
|
214
340
|
)
|
215
|
-
async def update_user_bulk(
|
341
|
+
async def update_user_bulk(
|
342
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
343
|
+
user_ids: list[str],
|
344
|
+
data: UserUpdateWithRoles,
|
345
|
+
) -> list[UserResponse]:
|
346
|
+
if not current_user.has_permission("user:update"):
|
347
|
+
raise ForbiddenError("Access denied")
|
216
348
|
return await auth_client.update_user_bulk(
|
217
|
-
user_ids, data.with_audit(updated_by=
|
349
|
+
user_ids, data.with_audit(updated_by=current_user.id)
|
218
350
|
)
|
219
351
|
|
220
352
|
@app.put(
|
221
353
|
"/api/v1/users/{user_id}",
|
222
354
|
response_model=UserResponse,
|
223
355
|
)
|
224
|
-
async def update_user(
|
225
|
-
|
356
|
+
async def update_user(
|
357
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
358
|
+
user_id: str,
|
359
|
+
data: UserUpdateWithRoles,
|
360
|
+
) -> UserResponse:
|
361
|
+
if not current_user.has_permission("user:update"):
|
362
|
+
raise ForbiddenError("Access denied")
|
363
|
+
return await auth_client.update_user(
|
364
|
+
user_id, data.with_audit(updated_by=current_user.id)
|
365
|
+
)
|
226
366
|
|
227
367
|
@app.delete(
|
228
368
|
"/api/v1/users/bulk",
|
229
369
|
response_model=list[UserResponse],
|
230
370
|
)
|
231
|
-
async def delete_user_bulk(
|
232
|
-
|
371
|
+
async def delete_user_bulk(
|
372
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
373
|
+
user_ids: list[str],
|
374
|
+
) -> list[UserResponse]:
|
375
|
+
if not current_user.has_permission("user:delete"):
|
376
|
+
raise ForbiddenError("Access denied")
|
377
|
+
return await auth_client.delete_user_bulk(user_ids, deleted_by=current_user.id)
|
233
378
|
|
234
379
|
@app.delete(
|
235
380
|
"/api/v1/users/{user_id}",
|
236
381
|
response_model=UserResponse,
|
237
382
|
)
|
238
|
-
async def delete_user(
|
239
|
-
|
383
|
+
async def delete_user(
|
384
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
385
|
+
user_id: str,
|
386
|
+
) -> UserResponse:
|
387
|
+
if not current_user.has_permission("user:delete"):
|
388
|
+
raise ForbiddenError("Access denied")
|
389
|
+
return await auth_client.delete_user(user_id, deleted_by=current_user.id)
|
@@ -0,0 +1,57 @@
|
|
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
|
+
async def get_current_user(
|
19
|
+
request: Request,
|
20
|
+
response: Response,
|
21
|
+
bearer_access_token: Annotated[str, Depends(oauth2_scheme)],
|
22
|
+
) -> AuthUserResponse:
|
23
|
+
# Bearer token exists
|
24
|
+
if bearer_access_token is not None and bearer_access_token != "":
|
25
|
+
return await auth_client.get_current_user(bearer_access_token)
|
26
|
+
cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
|
27
|
+
# Cookie exists
|
28
|
+
if cookie_access_token is not None and cookie_access_token != "":
|
29
|
+
cookie_user = await auth_client.get_current_user(cookie_access_token)
|
30
|
+
if cookie_user.is_guest:
|
31
|
+
# If user is guest, the cookie is not needed
|
32
|
+
unset_user_session_cookie(response)
|
33
|
+
return cookie_user
|
34
|
+
# No bearer token or cookie
|
35
|
+
return await auth_client.get_current_user("")
|
36
|
+
|
37
|
+
|
38
|
+
def set_user_session_cookie(response: Response, user_session: UserSessionResponse):
|
39
|
+
response.set_cookie(
|
40
|
+
key=APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
|
41
|
+
value=user_session.access_token,
|
42
|
+
httponly=True,
|
43
|
+
max_age=60 * APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
|
44
|
+
expires=user_session.access_token_expired_at.astimezone(datetime.timezone.utc),
|
45
|
+
)
|
46
|
+
response.set_cookie(
|
47
|
+
key=APP_AUTH_REFRESH_TOKEN_COOKIE_NAME,
|
48
|
+
value=user_session.refresh_token,
|
49
|
+
httponly=True,
|
50
|
+
max_age=60 * APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
|
51
|
+
expires=user_session.refresh_token_expired_at.astimezone(datetime.timezone.utc),
|
52
|
+
)
|
53
|
+
|
54
|
+
|
55
|
+
def unset_user_session_cookie(response: Response):
|
56
|
+
response.delete_cookie(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
|
57
|
+
response.delete_cookie(APP_AUTH_REFRESH_TOKEN_COOKIE_NAME)
|
@@ -90,6 +90,12 @@ class AuthUserResponse(UserResponse):
|
|
90
90
|
is_super_user: bool
|
91
91
|
is_guest: bool
|
92
92
|
|
93
|
+
def has_permission(self, permission_name: str):
|
94
|
+
return self.is_super_user or permission_name in self.permission_names
|
95
|
+
|
96
|
+
def has_role(self, role_name: str):
|
97
|
+
return self.is_super_user or role_name in self.role_names
|
98
|
+
|
93
99
|
|
94
100
|
class MultipleUserResponse(BaseModel):
|
95
101
|
data: list[UserResponse]
|
@@ -111,6 +117,9 @@ class UserTokenData(SQLModel):
|
|
111
117
|
class UserSessionResponse(SQLModel):
|
112
118
|
id: str
|
113
119
|
user_id: str
|
120
|
+
access_token: str
|
121
|
+
refresh_token: str
|
122
|
+
token_type: str
|
114
123
|
access_token_expired_at: datetime.datetime
|
115
124
|
refresh_token_expired_at: datetime.datetime
|
116
125
|
|