zrb 1.0.0b7__py3-none-any.whl → 1.0.0b9__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/__main__.py +3 -0
- zrb/builtin/git.py +15 -15
- zrb/builtin/git_subtree.py +6 -6
- zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +1 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/my_module/service/my_entity/my_entity_service.py +5 -5
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +9 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +100 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +32 -27
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -5
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +103 -61
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +130 -96
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +220 -13
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +27 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +2 -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/role.py +13 -12
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +55 -12
- zrb/task/cmd_task.py +4 -7
- zrb/util/cmd/command.py +41 -50
- zrb/util/git.py +18 -18
- zrb/util/git_subtree.py +6 -6
- {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/METADATA +2 -1
- {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/RECORD +39 -39
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
- {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b7.dist-info → zrb-1.0.0b9.dist-info}/entry_points.txt +0 -0
@@ -1,35 +1,27 @@
|
|
1
1
|
import datetime
|
2
|
-
from typing import Any
|
2
|
+
from typing import Any
|
3
3
|
|
4
4
|
import ulid
|
5
5
|
from my_app_name.common.base_db_repository import BaseDBRepository
|
6
|
-
from my_app_name.common.error import NotFoundError
|
7
|
-
from my_app_name.config import (
|
8
|
-
APP_AUTH_GUEST_USER,
|
9
|
-
APP_AUTH_GUEST_USER_PERMISSIONS,
|
10
|
-
APP_AUTH_SUPER_USER,
|
11
|
-
APP_AUTH_SUPER_USER_PASSWORD,
|
12
|
-
APP_MAX_PARALLEL_SESSION,
|
13
|
-
APP_SESSION_EXPIRE_MINUTES,
|
14
|
-
)
|
6
|
+
from my_app_name.common.error import NotFoundError, UnauthorizedError
|
15
7
|
from my_app_name.module.auth.service.user.repository.user_repository import (
|
16
8
|
UserRepository,
|
17
9
|
)
|
18
10
|
from my_app_name.schema.permission import Permission
|
19
11
|
from my_app_name.schema.role import Role, RolePermission
|
20
|
-
from my_app_name.schema.session import Session, SessionResponse
|
21
12
|
from my_app_name.schema.user import (
|
22
13
|
User,
|
23
14
|
UserCreateWithAudit,
|
24
15
|
UserResponse,
|
25
16
|
UserRole,
|
17
|
+
UserSession,
|
18
|
+
UserSessionResponse,
|
19
|
+
UserTokenData,
|
26
20
|
UserUpdateWithAudit,
|
27
21
|
)
|
28
22
|
from passlib.context import CryptContext
|
29
|
-
from sqlalchemy.
|
30
|
-
from
|
31
|
-
from sqlalchemy.sql import ClauseElement, ColumnElement, Select
|
32
|
-
from sqlmodel import SQLModel, delete, insert, select
|
23
|
+
from sqlalchemy.sql import Select
|
24
|
+
from sqlmodel import delete, insert, select, update
|
33
25
|
|
34
26
|
# Password hashing context
|
35
27
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
@@ -39,6 +31,11 @@ def hash_password(password: str) -> str:
|
|
39
31
|
return pwd_context.hash(password)
|
40
32
|
|
41
33
|
|
34
|
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
35
|
+
"""Verifies if a password matches the stored hash."""
|
36
|
+
return pwd_context.verify(plain_password, hashed_password)
|
37
|
+
|
38
|
+
|
42
39
|
class UserDBRepository(
|
43
40
|
BaseDBRepository[User, UserResponse, UserCreateWithAudit, UserUpdateWithAudit],
|
44
41
|
UserRepository,
|
@@ -50,46 +47,15 @@ class UserDBRepository(
|
|
50
47
|
entity_name = "user"
|
51
48
|
column_preprocessors = {"password": hash_password}
|
52
49
|
|
53
|
-
def __init__(
|
54
|
-
self,
|
55
|
-
engine: Engine | AsyncEngine,
|
56
|
-
super_user_username: str = APP_AUTH_SUPER_USER,
|
57
|
-
super_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
|
58
|
-
guest_user_username: str = APP_AUTH_GUEST_USER,
|
59
|
-
guest_user_password: str = APP_AUTH_SUPER_USER_PASSWORD,
|
60
|
-
guest_user_permission_names: list[str] = APP_AUTH_GUEST_USER_PERMISSIONS,
|
61
|
-
max_parallel_session: int = APP_MAX_PARALLEL_SESSION,
|
62
|
-
session_expire_minutes: int = APP_SESSION_EXPIRE_MINUTES,
|
63
|
-
filter_param_parser: (
|
64
|
-
Callable[[SQLModel, str], list[ClauseElement]] | None
|
65
|
-
) = None,
|
66
|
-
sort_param_parser: Callable[[SQLModel, str], list[ColumnElement]] | None = None,
|
67
|
-
):
|
68
|
-
super().__init__(
|
69
|
-
engine=engine,
|
70
|
-
filter_param_parser=filter_param_parser,
|
71
|
-
sort_param_parser=sort_param_parser,
|
72
|
-
)
|
73
|
-
self._super_user_username = super_user_username
|
74
|
-
self._super_user_passwored = super_user_password
|
75
|
-
self._guest_user_username = guest_user_username
|
76
|
-
self._guest_user_password = guest_user_password
|
77
|
-
self._guest_user_permission_names = guest_user_permission_names
|
78
|
-
self._max_parallel_session = max_parallel_session
|
79
|
-
self._session_expire_minutes = session_expire_minutes
|
80
|
-
self._super_user: User | None = None
|
81
|
-
self._guest_user: User | None = None
|
82
|
-
|
83
50
|
def _select(self) -> Select:
|
84
51
|
return (
|
85
|
-
select(User, Role, Permission,
|
52
|
+
select(User, Role, Permission, UserSession)
|
86
53
|
.join(UserRole, UserRole.user_id == User.id, isouter=True)
|
87
54
|
.join(Role, Role.id == UserRole.role_id, isouter=True)
|
88
55
|
.join(RolePermission, RolePermission.role_id == Role.id, isouter=True)
|
89
56
|
.join(
|
90
57
|
Permission, Permission.id == RolePermission.permission_id, isouter=True
|
91
58
|
)
|
92
|
-
.join(Session, Session.user_id == User.id)
|
93
59
|
)
|
94
60
|
|
95
61
|
def _rows_to_responses(self, rows: list[tuple[Any, ...]]) -> list[UserResponse]:
|
@@ -103,33 +69,43 @@ class UserDBRepository(
|
|
103
69
|
user_permission_map[user.id] = []
|
104
70
|
if role is not None and role.id not in user_role_map[user.id]:
|
105
71
|
user_role_map[user.id].append(role.id)
|
106
|
-
user_map[user.id]["roles"].append(role
|
72
|
+
user_map[user.id]["roles"].append(role)
|
107
73
|
if (
|
108
74
|
permission is not None
|
109
75
|
and permission.id not in user_permission_map[user.id]
|
110
76
|
):
|
111
77
|
user_permission_map[user.id].append(permission.id)
|
112
|
-
user_map[user.id]["permissions"].append(permission
|
78
|
+
user_map[user.id]["permissions"].append(permission)
|
113
79
|
return [
|
114
80
|
UserResponse(
|
115
81
|
**data["user"].model_dump(),
|
116
|
-
|
117
|
-
|
82
|
+
role_names=[role.name for role in data["roles"]],
|
83
|
+
permission_names=[
|
84
|
+
permission.name for permission in data["permissions"]
|
85
|
+
],
|
118
86
|
)
|
119
87
|
for data in user_map.values()
|
120
88
|
]
|
121
89
|
|
122
90
|
async def add_roles(self, data: dict[str, list[str]], created_by: str):
|
123
91
|
now = datetime.datetime.now(datetime.timezone.utc)
|
92
|
+
# get mapping from role names to role ids
|
93
|
+
all_role_names = {name for role_names in data.values() for name in role_names}
|
94
|
+
async with self._session_scope() as session:
|
95
|
+
result = await self._execute_statement(
|
96
|
+
session, select(Role.id, Role.name).where(Role.name.in_(all_role_names))
|
97
|
+
)
|
98
|
+
role_mapping = {row.name: row.id for row in result}
|
99
|
+
# Assemble data dict
|
124
100
|
data_dict_list: list[dict[str, Any]] = []
|
125
|
-
for user_id,
|
126
|
-
for
|
101
|
+
for user_id, role_names in data.items():
|
102
|
+
for role_name in role_names:
|
127
103
|
data_dict_list.append(
|
128
104
|
self._model_to_data_dict(
|
129
105
|
UserRole(
|
130
106
|
id=ulid.new().str,
|
131
107
|
user_id=user_id,
|
132
|
-
role_id=
|
108
|
+
role_id=role_mapping.get(role_name),
|
133
109
|
created_at=now,
|
134
110
|
created_by=created_by,
|
135
111
|
)
|
@@ -148,65 +124,123 @@ class UserDBRepository(
|
|
148
124
|
)
|
149
125
|
|
150
126
|
async def get_by_credentials(self, username: str, password: str) -> UserResponse:
|
151
|
-
|
152
|
-
|
153
|
-
User.username == username, User.
|
127
|
+
async with self._session_scope() as session:
|
128
|
+
result = await self._execute_statement(
|
129
|
+
session, select(User).where(User.username == username, User.active)
|
154
130
|
)
|
155
|
-
|
156
|
-
|
131
|
+
user = result.scalar_one_or_none()
|
132
|
+
if user is None or not verify_password(password, user.password):
|
133
|
+
raise UnauthorizedError("Invalid username or password")
|
134
|
+
return await self.get_by_id(user.id)
|
157
135
|
|
158
|
-
async def
|
159
|
-
|
160
|
-
lambda q: q.where(Session.token == token)
|
161
|
-
)
|
162
|
-
return self._ensure_one(rows)
|
163
|
-
|
164
|
-
async def add_token(self, user_id: str, token: str):
|
136
|
+
async def delete_expired_user_sessions(self, user_id: str):
|
137
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
165
138
|
async with self._session_scope() as session:
|
166
139
|
await self._execute_statement(
|
167
140
|
session,
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
"user_id": user_id,
|
172
|
-
"token": token,
|
173
|
-
"created_by": "system",
|
174
|
-
"created_at": datetime.datetime.now(datetime.timezone.utc),
|
175
|
-
}
|
141
|
+
delete(UserSession).where(
|
142
|
+
UserSession.user_id == user_id,
|
143
|
+
UserSession.refresh_token_expired_at < now,
|
176
144
|
),
|
177
145
|
)
|
178
146
|
|
179
|
-
async def
|
147
|
+
async def get_active_user_sessions(self, user_id: str) -> list[UserSessionResponse]:
|
148
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
180
149
|
async with self._session_scope() as session:
|
181
|
-
await self._execute_statement(
|
150
|
+
result = await self._execute_statement(
|
182
151
|
session,
|
183
|
-
|
184
|
-
|
152
|
+
select(UserSession).where(
|
153
|
+
UserSession.user_id == user_id,
|
154
|
+
UserSession.refresh_token_expired_at > now,
|
185
155
|
),
|
186
156
|
)
|
157
|
+
return [self._user_session_to_response(row[0]) for row in result.all()]
|
187
158
|
|
188
|
-
async def
|
159
|
+
async def get_user_session_by_access_token(
|
160
|
+
self, access_token: str
|
161
|
+
) -> UserSessionResponse:
|
162
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
163
|
+
async with self._session_scope() as session:
|
164
|
+
result = await self._execute_statement(
|
165
|
+
session,
|
166
|
+
select(UserSession).where(
|
167
|
+
UserSession.access_token == access_token,
|
168
|
+
UserSession.access_token_expired_at > now,
|
169
|
+
),
|
170
|
+
)
|
171
|
+
user_session = result.scalar_one_or_none()
|
172
|
+
if user_session is None:
|
173
|
+
raise NotFoundError("User session not found")
|
174
|
+
return self._user_session_to_response(user_session)
|
175
|
+
|
176
|
+
async def get_user_session_by_refresh_token(
|
177
|
+
self, refresh_token: str
|
178
|
+
) -> UserSessionResponse:
|
179
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
189
180
|
async with self._session_scope() as session:
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
181
|
+
result = await self._execute_statement(
|
182
|
+
session,
|
183
|
+
select(UserSession).where(
|
184
|
+
UserSession.refresh_token == refresh_token,
|
185
|
+
UserSession.refresh_token_expired_at > now,
|
186
|
+
),
|
187
|
+
)
|
188
|
+
user_session = result.scalar_one_or_none()
|
189
|
+
if user_session is None:
|
190
|
+
raise NotFoundError("User session not found")
|
191
|
+
return self._user_session_to_response(user_session)
|
192
|
+
|
193
|
+
async def create_user_session(
|
194
|
+
self, user_id: str, token_data: UserTokenData
|
195
|
+
) -> UserSessionResponse:
|
196
|
+
data_dict = self._model_to_data_dict(
|
197
|
+
token_data, user_id=user_id, id=ulid.new().str
|
198
|
+
)
|
198
199
|
async with self._session_scope() as session:
|
199
|
-
|
200
|
-
|
200
|
+
await self._execute_statement(
|
201
|
+
session, insert(UserSession).values(**data_dict)
|
202
|
+
)
|
203
|
+
result = await self._execute_statement(
|
204
|
+
session, select(UserSession).where(UserSession.id == data_dict["id"])
|
201
205
|
)
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
+
user_session = result.scalar_one_or_none()
|
207
|
+
if user_session is None:
|
208
|
+
raise NotFoundError("User session not found after created")
|
209
|
+
return self._user_session_to_response(user_session)
|
210
|
+
|
211
|
+
async def update_user_session(
|
212
|
+
self, user_id: str, session_id: str, token_data: UserTokenData
|
213
|
+
) -> UserSessionResponse:
|
214
|
+
data_dict = self._model_to_data_dict(token_data, user_id=user_id)
|
215
|
+
async with self._session_scope() as session:
|
206
216
|
await self._execute_statement(
|
207
217
|
session,
|
208
|
-
|
209
|
-
|
218
|
+
(
|
219
|
+
update(UserSession)
|
220
|
+
.where(UserSession.id == session_id)
|
221
|
+
.values(**data_dict)
|
210
222
|
),
|
211
223
|
)
|
212
|
-
|
224
|
+
result = await self._execute_statement(
|
225
|
+
session, select(UserSession).where(UserSession.id == session_id)
|
226
|
+
)
|
227
|
+
user_session = result.scalar_one_or_none()
|
228
|
+
if user_session is None:
|
229
|
+
raise NotFoundError("User session not found after created")
|
230
|
+
return self._user_session_to_response(user_session)
|
231
|
+
|
232
|
+
async def delete_user_sessions(self, session_ids: list[str]):
|
233
|
+
async with self._session_scope() as session:
|
234
|
+
await self._execute_statement(
|
235
|
+
session, delete(UserSession).where(UserSession.id.in_(session_ids))
|
236
|
+
)
|
237
|
+
|
238
|
+
def _user_session_to_response(
|
239
|
+
self, user_session: UserSession
|
240
|
+
) -> UserSessionResponse:
|
241
|
+
return UserSessionResponse(
|
242
|
+
id=user_session.id,
|
243
|
+
user_id=user_session.user_id,
|
244
|
+
access_token_expired_at=user_session.access_token_expired_at,
|
245
|
+
refresh_token_expired_at=user_session.refresh_token_expired_at,
|
246
|
+
)
|
@@ -1,10 +1,11 @@
|
|
1
1
|
from abc import ABC, abstractmethod
|
2
2
|
|
3
|
-
from my_app_name.schema.session import SessionResponse
|
4
3
|
from my_app_name.schema.user import (
|
5
4
|
User,
|
6
5
|
UserCreateWithAudit,
|
7
6
|
UserResponse,
|
7
|
+
UserSessionResponse,
|
8
|
+
UserTokenData,
|
8
9
|
UserUpdateWithAudit,
|
9
10
|
)
|
10
11
|
|
@@ -72,21 +73,37 @@ class UserRepository(ABC):
|
|
72
73
|
"""Get user by credential"""
|
73
74
|
|
74
75
|
@abstractmethod
|
75
|
-
async def
|
76
|
-
"""Get user
|
76
|
+
async def get_active_user_sessions(self, user_id: str) -> list[UserSessionResponse]:
|
77
|
+
"""Get user sessions"""
|
77
78
|
|
78
79
|
@abstractmethod
|
79
|
-
async def
|
80
|
-
|
80
|
+
async def get_user_session_by_access_token(
|
81
|
+
self, access_token: str
|
82
|
+
) -> UserSessionResponse:
|
83
|
+
"""Get user session by access token"""
|
81
84
|
|
82
85
|
@abstractmethod
|
83
|
-
async def
|
84
|
-
|
86
|
+
async def get_user_session_by_refresh_token(
|
87
|
+
self, refresh_token: str
|
88
|
+
) -> UserSessionResponse:
|
89
|
+
"""Get user session by refresh token"""
|
85
90
|
|
86
91
|
@abstractmethod
|
87
|
-
async def
|
88
|
-
|
92
|
+
async def create_user_session(
|
93
|
+
self, user_id: str, token_data: UserTokenData
|
94
|
+
) -> UserSessionResponse:
|
95
|
+
"""Create new user session"""
|
89
96
|
|
90
97
|
@abstractmethod
|
91
|
-
async def
|
92
|
-
|
98
|
+
async def update_user_session(
|
99
|
+
self, user_id: str, session_id: str, token_data: UserTokenData
|
100
|
+
) -> UserSessionResponse:
|
101
|
+
"""Update user session"""
|
102
|
+
|
103
|
+
@abstractmethod
|
104
|
+
async def delete_expired_user_sessions(self, user_id: str):
|
105
|
+
"""Delete expired user sessions"""
|
106
|
+
|
107
|
+
@abstractmethod
|
108
|
+
async def delete_user_sessions(self, session_ids: list[str]):
|
109
|
+
"""Delete user session"""
|