zrb 1.0.0b8__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/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 +2 -5
- zrb/util/cmd/command.py +39 -48
- {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/METADATA +2 -1
- {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/RECORD +35 -35
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
- {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/entry_points.txt +0 -0
@@ -1,22 +1,103 @@
|
|
1
|
+
import datetime
|
1
2
|
from logging import Logger
|
2
3
|
|
4
|
+
from jose import jwt
|
3
5
|
from my_app_name.common.base_service import BaseService
|
6
|
+
from my_app_name.common.error import ForbiddenError, NotFoundError
|
4
7
|
from my_app_name.module.auth.service.user.repository.user_repository import (
|
5
8
|
UserRepository,
|
6
9
|
)
|
7
10
|
from my_app_name.schema.user import (
|
11
|
+
AuthUserResponse,
|
8
12
|
MultipleUserResponse,
|
9
13
|
UserCreateWithRolesAndAudit,
|
14
|
+
UserCredentials,
|
10
15
|
UserResponse,
|
16
|
+
UserSessionResponse,
|
17
|
+
UserTokenData,
|
11
18
|
UserUpdateWithRolesAndAudit,
|
12
19
|
)
|
20
|
+
from pydantic import BaseModel
|
21
|
+
|
22
|
+
|
23
|
+
class UserServiceConfig(BaseModel):
|
24
|
+
super_user: str
|
25
|
+
super_user_password: str
|
26
|
+
guest_user: str = "guest"
|
27
|
+
guest_user_permissions: list[str] = []
|
28
|
+
max_parallel_session: int = 1
|
29
|
+
access_token_expire_minutes: int = 30
|
30
|
+
refresh_token_expire_minutes: int = 1440
|
31
|
+
secret_key: str = "my-secret-key"
|
32
|
+
prioritize_new_session: bool = True
|
13
33
|
|
14
34
|
|
15
35
|
class UserService(BaseService):
|
16
36
|
|
17
|
-
def __init__(
|
37
|
+
def __init__(
|
38
|
+
self, logger: Logger, user_repository: UserRepository, config: UserServiceConfig
|
39
|
+
):
|
18
40
|
super().__init__(logger)
|
19
41
|
self.user_repository = user_repository
|
42
|
+
self.config = config
|
43
|
+
|
44
|
+
@BaseService.route(
|
45
|
+
"/api/v1/current-user",
|
46
|
+
methods=["get"],
|
47
|
+
response_model=AuthUserResponse,
|
48
|
+
)
|
49
|
+
async def get_current_user(self, access_token: str) -> AuthUserResponse:
|
50
|
+
return self._get_auth_user_by_access_token(access_token)
|
51
|
+
|
52
|
+
@BaseService.route(
|
53
|
+
"/api/v1/user-sessions",
|
54
|
+
methods=["post"],
|
55
|
+
response_model=UserSessionResponse,
|
56
|
+
)
|
57
|
+
async def create_user_session(
|
58
|
+
self, credentials: UserCredentials
|
59
|
+
) -> UserSessionResponse:
|
60
|
+
current_user = await self._get_user_by_credentials(credentials)
|
61
|
+
await self.user_repository.delete_expired_user_sessions(current_user.id)
|
62
|
+
user_sessions = await self.user_repository.get_active_user_sessions(
|
63
|
+
current_user.id
|
64
|
+
)
|
65
|
+
user_session_count = len(user_sessions)
|
66
|
+
if user_session_count >= self.config.max_parallel_session:
|
67
|
+
await self._handle_excess_sessions(user_sessions)
|
68
|
+
token_data = self._create_user_token_data(current_user.username)
|
69
|
+
return await self.user_repository.create_user_session(
|
70
|
+
user_id=current_user.id, token_data=token_data
|
71
|
+
)
|
72
|
+
|
73
|
+
@BaseService.route(
|
74
|
+
"/api/v1/user-sessions",
|
75
|
+
methods=["put"],
|
76
|
+
response_model=UserSessionResponse,
|
77
|
+
)
|
78
|
+
async def update_user_session(self, refresh_token: str) -> UserSessionResponse:
|
79
|
+
current_user = await self._get_auth_user_by_refresh_token(refresh_token)
|
80
|
+
current_user_session = (
|
81
|
+
await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
|
82
|
+
)
|
83
|
+
token_data = self._create_user_token_data(current_user.username)
|
84
|
+
return await self.user_repository.update_user_session(
|
85
|
+
user_id=current_user.id,
|
86
|
+
session_id=current_user_session.id,
|
87
|
+
token_data=token_data,
|
88
|
+
)
|
89
|
+
|
90
|
+
@BaseService.route(
|
91
|
+
"/api/v1/user-sessions",
|
92
|
+
methods=["delete"],
|
93
|
+
response_model=UserSessionResponse,
|
94
|
+
)
|
95
|
+
async def delete_user_session(self, refresh_token: str) -> UserSessionResponse:
|
96
|
+
current_user_session = (
|
97
|
+
await self.user_respository.get_user_sesion_by_refresh_token(refresh_token)
|
98
|
+
)
|
99
|
+
await self.user_repository.delete_user_sessions([current_user_session.id])
|
100
|
+
return current_user_session
|
20
101
|
|
21
102
|
@BaseService.route(
|
22
103
|
"/api/v1/users/{user_id}",
|
@@ -50,13 +131,13 @@ class UserService(BaseService):
|
|
50
131
|
async def create_user_bulk(
|
51
132
|
self, data: list[UserCreateWithRolesAndAudit]
|
52
133
|
) -> list[UserResponse]:
|
53
|
-
|
134
|
+
role_names = [row.get_role_names() for row in data]
|
54
135
|
data = [row.get_user_create_with_audit() for row in data]
|
55
136
|
users = await self.user_repository.create_bulk(data)
|
56
137
|
if len(users) > 0:
|
57
138
|
created_by = users[0].created_by
|
58
139
|
await self.user_repository.add_roles(
|
59
|
-
data={user.id:
|
140
|
+
data={user.id: role_names[i] for i, user in enumerate(users)},
|
60
141
|
created_by=created_by,
|
61
142
|
)
|
62
143
|
return await self.user_repository.get_by_ids([user.id for user in users])
|
@@ -67,30 +148,30 @@ class UserService(BaseService):
|
|
67
148
|
response_model=UserResponse,
|
68
149
|
)
|
69
150
|
async def create_user(self, data: UserCreateWithRolesAndAudit) -> UserResponse:
|
70
|
-
|
151
|
+
role_names = data.get_role_names()
|
71
152
|
data = data.get_user_create_with_audit()
|
72
153
|
user = await self.user_repository.create(data)
|
73
154
|
await self.user_repository.add_roles(
|
74
|
-
data={user.id:
|
155
|
+
data={user.id: role_names}, created_by=user.created_by
|
75
156
|
)
|
76
157
|
return await self.user_repository.get_by_id(user.id)
|
77
158
|
|
78
159
|
@BaseService.route(
|
79
160
|
"/api/v1/users/bulk",
|
80
161
|
methods=["put"],
|
81
|
-
response_model=UserResponse,
|
162
|
+
response_model=list[UserResponse],
|
82
163
|
)
|
83
164
|
async def update_user_bulk(
|
84
165
|
self, user_ids: list[str], data: UserUpdateWithRolesAndAudit
|
85
|
-
) -> UserResponse:
|
86
|
-
|
166
|
+
) -> list[UserResponse]:
|
167
|
+
role_names = [row.get_role_names() for row in data]
|
87
168
|
user_data = [row.get_user_create_with_audit() for row in data]
|
88
169
|
await self.user_repository.update_bulk(user_ids, user_data)
|
89
170
|
if len(user_ids) > 0:
|
90
171
|
updated_by = user_data[0].updated_by
|
91
172
|
await self.user_repository.remove_all_roles(user_ids)
|
92
173
|
await self.user_repository.add_roles(
|
93
|
-
data={user_id:
|
174
|
+
data={user_id: role_names[i] for i, user_id in enumerate(user_ids)},
|
94
175
|
updated_by=updated_by,
|
95
176
|
)
|
96
177
|
return await self.user_repository.get_by_ids(user_ids)
|
@@ -103,23 +184,23 @@ class UserService(BaseService):
|
|
103
184
|
async def update_user(
|
104
185
|
self, user_id: str, data: UserUpdateWithRolesAndAudit
|
105
186
|
) -> UserResponse:
|
106
|
-
|
187
|
+
role_names = data.get_role_names()
|
107
188
|
user_data = data.get_user_update_with_audit()
|
108
189
|
await self.user_repository.update(user_id, user_data)
|
109
190
|
await self.user_repository.remove_all_roles([user_id])
|
110
191
|
await self.user_repository.add_roles(
|
111
|
-
data={user_id:
|
192
|
+
data={user_id: role_names}, created_by=user_data.updated_by
|
112
193
|
)
|
113
194
|
return await self.user_repository.get_by_id(user_id)
|
114
195
|
|
115
196
|
@BaseService.route(
|
116
197
|
"/api/v1/users/bulk",
|
117
198
|
methods=["delete"],
|
118
|
-
response_model=UserResponse,
|
199
|
+
response_model=list[UserResponse],
|
119
200
|
)
|
120
201
|
async def delete_user_bulk(
|
121
202
|
self, user_ids: list[str], deleted_by: str
|
122
|
-
) -> UserResponse:
|
203
|
+
) -> list[UserResponse]:
|
123
204
|
roles = await self.user_repository.get_by_ids(user_ids)
|
124
205
|
await self.user_repository.delete_bulk(user_ids)
|
125
206
|
await self.user_repository.remove_all_roles(user_ids)
|
@@ -135,3 +216,129 @@ class UserService(BaseService):
|
|
135
216
|
await self.user_repository.delete(user_id)
|
136
217
|
await self.user_repository.remove_all_roles([user_id])
|
137
218
|
return user
|
219
|
+
|
220
|
+
async def _get_auth_user_by_refresh_token(
|
221
|
+
self, refresh_token: str
|
222
|
+
) -> AuthUserResponse:
|
223
|
+
if refresh_token is None or refresh_token == "":
|
224
|
+
raise NotFoundError("User not found")
|
225
|
+
user_session = await self.user_repository.get_user_session_by_refresh_token(
|
226
|
+
refresh_token
|
227
|
+
)
|
228
|
+
user_id = user_session.user_id
|
229
|
+
if user_id == self.config.super_user:
|
230
|
+
return self._get_super_user()
|
231
|
+
user = await self.user_repository.get_by_id(user_id)
|
232
|
+
return self._to_auth_user_response(user)
|
233
|
+
|
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
|
+
async def _get_user_by_credentials(
|
252
|
+
self, credentials: UserCredentials
|
253
|
+
) -> AuthUserResponse:
|
254
|
+
if (
|
255
|
+
credentials.username == self.config.super_user
|
256
|
+
and credentials.password == self.config.super_user_password
|
257
|
+
):
|
258
|
+
return self._get_super_user()
|
259
|
+
user = await self.user_repository.get_by_credentials(
|
260
|
+
username=credentials.username,
|
261
|
+
password=credentials.password,
|
262
|
+
)
|
263
|
+
return self._to_auth_user_response(user)
|
264
|
+
|
265
|
+
def _to_auth_user_response(self, user_response: UserResponse) -> AuthUserResponse:
|
266
|
+
return AuthUserResponse(
|
267
|
+
**user_response.model_dump(), is_guest=False, is_super_user=False
|
268
|
+
)
|
269
|
+
|
270
|
+
def _get_guest_user(self):
|
271
|
+
return AuthUserResponse(
|
272
|
+
id=self.config.guest_user,
|
273
|
+
username=self.config.guest_user,
|
274
|
+
active=True,
|
275
|
+
role_names=[],
|
276
|
+
permission_names=self.config.guest_user_permissions,
|
277
|
+
is_guest=True,
|
278
|
+
is_super_user=False,
|
279
|
+
)
|
280
|
+
|
281
|
+
def _get_super_user(self):
|
282
|
+
return AuthUserResponse(
|
283
|
+
id=self.config.super_user,
|
284
|
+
username=self.config.super_user,
|
285
|
+
active=True,
|
286
|
+
role_names=[],
|
287
|
+
permission_names=[],
|
288
|
+
is_guest=False,
|
289
|
+
is_super_user=True,
|
290
|
+
)
|
291
|
+
|
292
|
+
async def _handle_excess_sessions(self, active_sessions: list[UserSessionResponse]):
|
293
|
+
"""Handles excess user sessions by deleting the oldest if necessary."""
|
294
|
+
if not self.config.prioritize_new_session:
|
295
|
+
raise ForbiddenError("No additional session allowed")
|
296
|
+
# Sort sessions by expiration and remove the oldest ones
|
297
|
+
sessions_to_delete = sorted(
|
298
|
+
active_sessions, key=lambda s: s.refresh_token_expired_at
|
299
|
+
)
|
300
|
+
excess_count = len(active_sessions) + 1 - self.config.max_parallel_session
|
301
|
+
await self.user_repository.delete_user_sessions(
|
302
|
+
[session.id for session in sessions_to_delete[:excess_count]]
|
303
|
+
)
|
304
|
+
|
305
|
+
def _create_user_token_data(self, username: str) -> UserTokenData:
|
306
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
307
|
+
access_token_expire_at = now + datetime.timedelta(
|
308
|
+
minutes=self.config.access_token_expire_minutes
|
309
|
+
)
|
310
|
+
refresh_token_expire_at = now + datetime.timedelta(
|
311
|
+
minutes=self.config.refresh_token_expire_minutes
|
312
|
+
)
|
313
|
+
return UserTokenData(
|
314
|
+
access_token=self._generate_access_token(
|
315
|
+
username=username,
|
316
|
+
expire_at=access_token_expire_at,
|
317
|
+
),
|
318
|
+
refresh_token=self._generate_refresh_token(
|
319
|
+
username=username,
|
320
|
+
expire_at=refresh_token_expire_at,
|
321
|
+
),
|
322
|
+
access_token_expired_at=access_token_expire_at,
|
323
|
+
refresh_token_expired_at=refresh_token_expire_at,
|
324
|
+
)
|
325
|
+
|
326
|
+
def _generate_access_token(
|
327
|
+
self, username: str, expire_at: datetime.datetime
|
328
|
+
) -> str:
|
329
|
+
return self._generate_user_token(
|
330
|
+
username=username, expire_at=expire_at, token_type="access"
|
331
|
+
)
|
332
|
+
|
333
|
+
def _generate_refresh_token(
|
334
|
+
self, username: str, expire_at: datetime.datetime
|
335
|
+
) -> str:
|
336
|
+
return self._generate_user_token(
|
337
|
+
username=username, expire_at=expire_at, token_type="refresh"
|
338
|
+
)
|
339
|
+
|
340
|
+
def _generate_user_token(
|
341
|
+
self, username: str, expire_at: datetime.datetime, token_type: str
|
342
|
+
) -> str:
|
343
|
+
to_encode = {"sub": username, "exp": expire_at, "type": token_type}
|
344
|
+
return jwt.encode(to_encode, self.config.secret_key)
|
@@ -1,7 +1,35 @@
|
|
1
1
|
from my_app_name.common.logger_factory import logger
|
2
|
+
from my_app_name.config import (
|
3
|
+
APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
|
4
|
+
APP_AUTH_GUEST_USER,
|
5
|
+
APP_AUTH_GUEST_USER_PERMISSIONS,
|
6
|
+
APP_AUTH_MAX_PARALLEL_SESSION,
|
7
|
+
APP_AUTH_PRIORITIZE_NEW_SESSION,
|
8
|
+
APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
|
9
|
+
APP_AUTH_SECRET_KEY,
|
10
|
+
APP_AUTH_SUPER_USER,
|
11
|
+
APP_AUTH_SUPER_USER_PASSWORD,
|
12
|
+
)
|
2
13
|
from my_app_name.module.auth.service.user.repository.user_repository_factory import (
|
3
14
|
user_repository,
|
4
15
|
)
|
5
|
-
from my_app_name.module.auth.service.user.user_service import
|
16
|
+
from my_app_name.module.auth.service.user.user_service import (
|
17
|
+
UserService,
|
18
|
+
UserServiceConfig,
|
19
|
+
)
|
6
20
|
|
7
|
-
user_service = UserService(
|
21
|
+
user_service = UserService(
|
22
|
+
logger,
|
23
|
+
user_repository=user_repository,
|
24
|
+
config=UserServiceConfig(
|
25
|
+
super_user=APP_AUTH_SUPER_USER,
|
26
|
+
super_user_password=APP_AUTH_SUPER_USER_PASSWORD,
|
27
|
+
guest_user=APP_AUTH_GUEST_USER,
|
28
|
+
guest_user_permissions=APP_AUTH_GUEST_USER_PERMISSIONS,
|
29
|
+
max_parallel_session=APP_AUTH_MAX_PARALLEL_SESSION,
|
30
|
+
access_token_expire_minutes=APP_AUTH_ACCESS_TOKEN_EXPIRE_MINUTES,
|
31
|
+
refresh_token_expire_minutes=APP_AUTH_REFRESH_TOKEN_EXPIRE_MINUTES,
|
32
|
+
secret_key=APP_AUTH_SECRET_KEY,
|
33
|
+
prioritize_new_session=APP_AUTH_PRIORITIZE_NEW_SESSION,
|
34
|
+
),
|
35
|
+
)
|
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
from
|
1
|
+
from typing import Annotated
|
2
|
+
|
3
|
+
from fastapi import Depends, FastAPI, Response
|
4
|
+
from fastapi.security import OAuth2PasswordRequestForm
|
2
5
|
from my_app_name.module.auth.client.auth_client_factory import auth_client
|
3
6
|
from my_app_name.schema.permission import (
|
4
7
|
MultiplePermissionResponse,
|
@@ -15,13 +18,35 @@ from my_app_name.schema.role import (
|
|
15
18
|
from my_app_name.schema.user import (
|
16
19
|
MultipleUserResponse,
|
17
20
|
UserCreateWithRoles,
|
21
|
+
UserCredentials,
|
18
22
|
UserResponse,
|
23
|
+
UserSessionResponse,
|
19
24
|
UserUpdateWithRoles,
|
20
25
|
)
|
21
26
|
|
22
27
|
|
23
28
|
def serve_auth_route(app: FastAPI):
|
24
29
|
|
30
|
+
@app.post("/api/v1/user-sessions", response_model=UserSessionResponse)
|
31
|
+
async def create_user_session(
|
32
|
+
response: Response, form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
|
33
|
+
) -> UserSessionResponse:
|
34
|
+
user_session = await auth_client.create_user_session(
|
35
|
+
UserCredentials(
|
36
|
+
username=form_data.username,
|
37
|
+
password=form_data.password,
|
38
|
+
)
|
39
|
+
)
|
40
|
+
return user_session
|
41
|
+
|
42
|
+
@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)
|
45
|
+
|
46
|
+
@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)
|
49
|
+
|
25
50
|
# Permission routes
|
26
51
|
|
27
52
|
@app.get("/api/v1/permissions", response_model=MultiplePermissionResponse)
|
@@ -172,7 +197,7 @@ def serve_auth_route(app: FastAPI):
|
|
172
197
|
response_model=list[UserResponse],
|
173
198
|
)
|
174
199
|
async def create_user_bulk(data: list[UserCreateWithRoles]):
|
175
|
-
return await auth_client.
|
200
|
+
return await auth_client.create_user_bulk(
|
176
201
|
[row.with_audit(created_by="system") for row in data]
|
177
202
|
)
|
178
203
|
|
@@ -42,6 +42,7 @@ class MultiplePermissionResponse(BaseModel):
|
|
42
42
|
|
43
43
|
|
44
44
|
class Permission(SQLModel, table=True):
|
45
|
+
__tablename__ = "permissions"
|
45
46
|
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
46
47
|
created_at: datetime.datetime | None = Field(index=True)
|
47
48
|
created_by: str | None = Field(index=True)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import datetime
|
2
2
|
|
3
3
|
import ulid
|
4
|
-
from my_app_name.schema.permission import Permission
|
5
4
|
from pydantic import BaseModel
|
6
5
|
from sqlmodel import Field, SQLModel
|
7
6
|
|
@@ -22,7 +21,7 @@ class RoleCreateWithAudit(RoleCreate):
|
|
22
21
|
|
23
22
|
|
24
23
|
class RoleCreateWithPermissions(RoleCreate):
|
25
|
-
|
24
|
+
permission_names: list[str] | None = None
|
26
25
|
|
27
26
|
def with_audit(self, created_by: str) -> "RoleCreateWithPermissionsAndAudit":
|
28
27
|
return RoleCreateWithPermissionsAndAudit(
|
@@ -37,14 +36,14 @@ class RoleCreateWithPermissionsAndAudit(RoleCreateWithPermissions):
|
|
37
36
|
data = {
|
38
37
|
key: val
|
39
38
|
for key, val in self.model_dump().items()
|
40
|
-
if key != "
|
39
|
+
if key != "permission_names"
|
41
40
|
}
|
42
41
|
return RoleCreateWithAudit(**data)
|
43
42
|
|
44
|
-
def
|
45
|
-
if self.
|
43
|
+
def get_permission_names(self) -> list[str]:
|
44
|
+
if self.permission_names is None:
|
46
45
|
return []
|
47
|
-
return self.
|
46
|
+
return self.permission_names
|
48
47
|
|
49
48
|
|
50
49
|
class RoleUpdate(SQLModel):
|
@@ -60,7 +59,7 @@ class RoleUpdateWithAudit(RoleUpdate):
|
|
60
59
|
|
61
60
|
|
62
61
|
class RoleUpdateWithPermissions(RoleUpdate):
|
63
|
-
|
62
|
+
permission_names: list[str] | None = None
|
64
63
|
|
65
64
|
def with_audit(self, updated_by: str) -> "RoleUpdateWithPermissionsAndAudit":
|
66
65
|
return RoleUpdateWithPermissionsAndAudit(
|
@@ -75,19 +74,19 @@ class RoleUpdateWithPermissionsAndAudit(RoleUpdateWithPermissions):
|
|
75
74
|
data = {
|
76
75
|
key: val
|
77
76
|
for key, val in self.model_dump().items()
|
78
|
-
if key != "
|
77
|
+
if key != "permission_names"
|
79
78
|
}
|
80
79
|
return RoleUpdateWithAudit(**data)
|
81
80
|
|
82
|
-
def
|
83
|
-
if self.
|
81
|
+
def get_permission_names(self) -> list[str]:
|
82
|
+
if self.permission_names is None:
|
84
83
|
return []
|
85
|
-
return self.
|
84
|
+
return self.permission_names
|
86
85
|
|
87
86
|
|
88
87
|
class RoleResponse(RoleBase):
|
89
88
|
id: str
|
90
|
-
|
89
|
+
permission_names: list[str]
|
91
90
|
|
92
91
|
|
93
92
|
class MultipleRoleResponse(BaseModel):
|
@@ -96,6 +95,7 @@ class MultipleRoleResponse(BaseModel):
|
|
96
95
|
|
97
96
|
|
98
97
|
class Role(SQLModel, table=True):
|
98
|
+
__tablename__ = "roles"
|
99
99
|
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
100
100
|
created_at: datetime.datetime | None = Field(index=True)
|
101
101
|
created_by: str | None = Field(index=True)
|
@@ -106,6 +106,7 @@ class Role(SQLModel, table=True):
|
|
106
106
|
|
107
107
|
|
108
108
|
class RolePermission(SQLModel, table=True):
|
109
|
+
__tablename__ = "role_permissions"
|
109
110
|
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
110
111
|
role_id: str = Field(index=True)
|
111
112
|
permission_id: str = Field(index=True)
|
@@ -9,6 +9,7 @@ from sqlmodel import Field, SQLModel
|
|
9
9
|
|
10
10
|
class UserBase(SQLModel):
|
11
11
|
username: str
|
12
|
+
active: bool
|
12
13
|
|
13
14
|
|
14
15
|
class UserCreate(UserBase):
|
@@ -23,7 +24,7 @@ class UserCreateWithAudit(UserCreate):
|
|
23
24
|
|
24
25
|
|
25
26
|
class UserCreateWithRoles(UserCreate):
|
26
|
-
|
27
|
+
role_names: list[str] | None = None
|
27
28
|
|
28
29
|
def with_audit(self, created_by: str) -> "UserCreateWithRolesAndAudit":
|
29
30
|
return UserCreateWithRolesAndAudit(**self.model_dump(), created_by=created_by)
|
@@ -33,18 +34,21 @@ class UserCreateWithRolesAndAudit(UserCreateWithRoles):
|
|
33
34
|
created_by: str
|
34
35
|
|
35
36
|
def get_user_create_with_audit(self) -> UserCreateWithAudit:
|
36
|
-
data = {
|
37
|
+
data = {
|
38
|
+
key: val for key, val in self.model_dump().items() if key != "role_names"
|
39
|
+
}
|
37
40
|
return UserCreateWithAudit(**data)
|
38
41
|
|
39
|
-
def
|
40
|
-
if self.
|
42
|
+
def get_role_names(self) -> list[str]:
|
43
|
+
if self.role_names is None:
|
41
44
|
return []
|
42
|
-
return self.
|
45
|
+
return self.role_names
|
43
46
|
|
44
47
|
|
45
48
|
class UserUpdate(SQLModel):
|
46
49
|
username: str | None = None
|
47
50
|
password: str | None = None
|
51
|
+
active: bool | None = None
|
48
52
|
|
49
53
|
def with_audit(self, updated_by: str) -> "UserUpdateWithAudit":
|
50
54
|
return UserUpdateWithAudit(**self.model_dump(), updated_by=updated_by)
|
@@ -55,7 +59,7 @@ class UserUpdateWithAudit(UserUpdate):
|
|
55
59
|
|
56
60
|
|
57
61
|
class UserUpdateWithRoles(UserUpdate):
|
58
|
-
|
62
|
+
role_names: list[str] | None = None
|
59
63
|
|
60
64
|
def with_audit(self, updated_by: str) -> "UserUpdateWithRolesAndAudit":
|
61
65
|
return UserUpdateWithRolesAndAudit(**self.model_dump(), updated_by=updated_by)
|
@@ -65,19 +69,26 @@ class UserUpdateWithRolesAndAudit(UserUpdateWithRoles):
|
|
65
69
|
updated_by: str
|
66
70
|
|
67
71
|
def get_user_update_with_audit(self) -> UserUpdateWithAudit:
|
68
|
-
data = {
|
72
|
+
data = {
|
73
|
+
key: val for key, val in self.model_dump().items() if key != "role_names"
|
74
|
+
}
|
69
75
|
return UserUpdateWithAudit(**data)
|
70
76
|
|
71
|
-
def
|
72
|
-
if self.
|
77
|
+
def get_role_names(self) -> list[str]:
|
78
|
+
if self.role_names is None:
|
73
79
|
return []
|
74
|
-
return self.
|
80
|
+
return self.role_names
|
75
81
|
|
76
82
|
|
77
83
|
class UserResponse(UserBase):
|
78
84
|
id: str
|
79
|
-
|
80
|
-
|
85
|
+
role_names: list[str]
|
86
|
+
permission_names: list[str]
|
87
|
+
|
88
|
+
|
89
|
+
class AuthUserResponse(UserResponse):
|
90
|
+
is_super_user: bool
|
91
|
+
is_guest: bool
|
81
92
|
|
82
93
|
|
83
94
|
class MultipleUserResponse(BaseModel):
|
@@ -85,7 +96,27 @@ class MultipleUserResponse(BaseModel):
|
|
85
96
|
count: int
|
86
97
|
|
87
98
|
|
99
|
+
class UserCredentials(SQLModel):
|
100
|
+
username: str
|
101
|
+
password: str
|
102
|
+
|
103
|
+
|
104
|
+
class UserTokenData(SQLModel):
|
105
|
+
access_token: str
|
106
|
+
refresh_token: str
|
107
|
+
access_token_expired_at: datetime.datetime
|
108
|
+
refresh_token_expired_at: datetime.datetime
|
109
|
+
|
110
|
+
|
111
|
+
class UserSessionResponse(SQLModel):
|
112
|
+
id: str
|
113
|
+
user_id: str
|
114
|
+
access_token_expired_at: datetime.datetime
|
115
|
+
refresh_token_expired_at: datetime.datetime
|
116
|
+
|
117
|
+
|
88
118
|
class User(SQLModel, table=True):
|
119
|
+
__tablename__ = "users"
|
89
120
|
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
90
121
|
created_at: datetime.datetime = Field(index=True)
|
91
122
|
created_by: str = Field(index=True)
|
@@ -93,11 +124,23 @@ class User(SQLModel, table=True):
|
|
93
124
|
updated_by: str | None = Field(index=True)
|
94
125
|
username: str = Field(index=True, unique=True)
|
95
126
|
password: str
|
127
|
+
active: bool = Field(index=True)
|
96
128
|
|
97
129
|
|
98
130
|
class UserRole(SQLModel, table=True):
|
131
|
+
__tablename__ = "user_roles"
|
99
132
|
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
100
133
|
user_id: str = Field(index=True)
|
101
134
|
role_id: str = Field(index=True)
|
102
135
|
created_at: datetime.datetime | None
|
103
136
|
created_by: str | None
|
137
|
+
|
138
|
+
|
139
|
+
class UserSession(SQLModel, table=True):
|
140
|
+
__tablename__ = "user_sessions"
|
141
|
+
id: str = Field(default_factory=lambda: ulid.new().str, primary_key=True)
|
142
|
+
user_id: str = Field(index=True)
|
143
|
+
access_token: str = Field(index=True)
|
144
|
+
refresh_token: str = Field(index=True)
|
145
|
+
access_token_expired_at: datetime.datetime = Field(index=True)
|
146
|
+
refresh_token_expired_at: datetime.datetime = Field(index=True)
|
zrb/task/cmd_task.py
CHANGED
@@ -141,11 +141,8 @@ class CmdTask(BaseTask):
|
|
141
141
|
max_error_line=self._max_error_line,
|
142
142
|
)
|
143
143
|
# Check for errors
|
144
|
-
if return_code
|
145
|
-
|
146
|
-
raise Exception(
|
147
|
-
f"Process {self._name} exited ({return_code}): {cmd_result.error}"
|
148
|
-
)
|
144
|
+
if return_code > 0:
|
145
|
+
raise Exception(f"Process {self._name} exited ({return_code})")
|
149
146
|
ctx.log_info(f"Exit status: {return_code}")
|
150
147
|
return cmd_result
|
151
148
|
|