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.
Files changed (36) hide show
  1. zrb/__main__.py +3 -0
  2. zrb/builtin/project/add/fastapp/fastapp_task.py +1 -0
  3. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +1 -0
  4. 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
  5. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/schema/my_entity.py +1 -0
  6. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/input.py +8 -0
  7. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task.py +9 -2
  8. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +100 -0
  9. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/util.py +6 -86
  10. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_db_repository.py +27 -11
  11. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +32 -27
  12. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/error.py +15 -0
  13. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +22 -5
  14. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/client/auth_client.py +21 -0
  15. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +103 -61
  16. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration_metadata.py +3 -4
  17. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/route.py +15 -14
  18. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/permission/permission_service.py +4 -4
  19. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/repository/role_db_repository.py +24 -5
  20. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/role/role_service.py +14 -12
  21. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +130 -96
  22. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_repository.py +28 -11
  23. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +220 -13
  24. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service_factory.py +30 -2
  25. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +27 -2
  26. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +2 -1
  27. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
  28. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/role.py +13 -12
  29. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +55 -12
  30. zrb/task/cmd_task.py +2 -5
  31. zrb/util/cmd/command.py +39 -48
  32. {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/METADATA +2 -1
  33. {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/RECORD +35 -35
  34. zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/session.py +0 -48
  35. {zrb-1.0.0b8.dist-info → zrb-1.0.0b9.dist-info}/WHEEL +0 -0
  36. {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__(self, logger: Logger, user_repository: UserRepository):
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
- role_ids = [row.get_role_ids() for row in data]
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: role_ids[i] for i, user in enumerate(data)},
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
- role_ids = data.get_role_ids()
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: role_ids}, created_by=user.created_by
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
- role_ids = [row.get_role_ids() for row in data]
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: role_ids[i] for i, user_id in enumerate(user_ids)},
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
- role_ids = data.get_role_ids()
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: role_ids}, created_by=user_data.updated_by
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 UserService
16
+ from my_app_name.module.auth.service.user.user_service import (
17
+ UserService,
18
+ UserServiceConfig,
19
+ )
6
20
 
7
- user_service = UserService(logger, user_repository=user_repository)
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
+ )
@@ -1,4 +1,7 @@
1
- from fastapi import FastAPI
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.create_user(
200
+ return await auth_client.create_user_bulk(
176
201
  [row.with_audit(created_by="system") for row in data]
177
202
  )
178
203
 
@@ -3,4 +3,5 @@ alembic~=1.14.0
3
3
  sqlmodel~=0.0.22
4
4
  ulid-py~=1.1.0
5
5
  passlib~=1.7.4
6
- Jinja2==3.1.5
6
+ Jinja2~=3.1.5
7
+ python-jose~=3.3.0
@@ -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
- permission_ids: list[str] | None = None
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 != "permission_ids"
39
+ if key != "permission_names"
41
40
  }
42
41
  return RoleCreateWithAudit(**data)
43
42
 
44
- def get_permission_ids(self) -> list[str]:
45
- if self.permission_ids is None:
43
+ def get_permission_names(self) -> list[str]:
44
+ if self.permission_names is None:
46
45
  return []
47
- return self.permission_ids
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
- permission_ids: list[str] | None = None
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 != "permission_ids"
77
+ if key != "permission_names"
79
78
  }
80
79
  return RoleUpdateWithAudit(**data)
81
80
 
82
- def get_permission_ids(self) -> list[str]:
83
- if self.permission_ids is None:
81
+ def get_permission_names(self) -> list[str]:
82
+ if self.permission_names is None:
84
83
  return []
85
- return self.permission_ids
84
+ return self.permission_names
86
85
 
87
86
 
88
87
  class RoleResponse(RoleBase):
89
88
  id: str
90
- permissions: list[Permission]
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
- role_ids: list[str] | None = None
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 = {key: val for key, val in self.model_dump().items() if key != "role_ids"}
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 get_role_ids(self) -> list[str]:
40
- if self.role_ids is None:
42
+ def get_role_names(self) -> list[str]:
43
+ if self.role_names is None:
41
44
  return []
42
- return self.role_ids
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
- role_ids: list[str] | None = None
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 = {key: val for key, val in self.model_dump().items() if key != "role_ids"}
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 get_role_ids(self) -> list[str]:
72
- if self.role_ids is None:
77
+ def get_role_names(self) -> list[str]:
78
+ if self.role_names is None:
73
79
  return []
74
- return self.role_ids
80
+ return self.role_names
75
81
 
76
82
 
77
83
  class UserResponse(UserBase):
78
84
  id: str
79
- roles: list[Role]
80
- permissions: list[Permission]
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 != 0:
145
- ctx.log_error(f"Exit status: {return_code}")
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