From 0d23cc3d05b54e1f69b8631fad0b9bd01b7cd479 Mon Sep 17 00:00:00 2001 From: keep <1603421097@qq.com> Date: Wed, 15 Apr 2026 11:42:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增权限管理模块,包括权限模型、路由、服务和API端点 - 扩展用户模型,添加权限检查方法 - 添加角色与权限的多对多关联 - 实现前端权限管理界面 - 添加权限验证依赖项 - 更新数据库迁移脚本 - 完善异常处理和错误消息 - 添加权限相关测试用例 - 更新API文档和前端类型定义 --- .../alembic/__pycache__/env.cpython-312.pyc | Bin 0 -> 3807 bytes .../09fac9965b5b_init_tables.cpython-312.pyc | Bin 0 -> 4875 bytes ...5f6_add_permissions_tables.cpython-312.pyc | Bin 0 -> 3483 bytes .../a1b2c3d4e5f6_add_permissions_tables.py | 53 ++++ .../app/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 171 bytes backend/app/__pycache__/main.cpython-312.pyc | Bin 0 -> 6935 bytes .../app/api/__pycache__/deps.cpython-312.pyc | Bin 0 -> 9055 bytes .../api/__pycache__/routers.cpython-312.pyc | Bin 0 -> 987 bytes backend/app/api/deps.py | 75 +++++ .../__pycache__/auth.cpython-312.pyc | Bin 0 -> 4853 bytes .../__pycache__/permissions.cpython-312.pyc | Bin 0 -> 6319 bytes .../__pycache__/roles.cpython-312.pyc | Bin 0 -> 9136 bytes .../__pycache__/users.cpython-312.pyc | Bin 0 -> 6527 bytes backend/app/api/endpoints/permissions.py | 145 +++++++++ backend/app/api/endpoints/roles.py | 220 +++++++++++++ backend/app/api/routers.py | 4 +- .../app/core/__pycache__/app.cpython-312.pyc | Bin 0 -> 1681 bytes .../app/core/__pycache__/db.cpython-312.pyc | Bin 0 -> 7919 bytes .../core/__pycache__/events.cpython-312.pyc | Bin 0 -> 2819 bytes .../core/__pycache__/limiter.cpython-312.pyc | Bin 0 -> 2377 bytes .../__pycache__/middleware.cpython-312.pyc | Bin 0 -> 5444 bytes .../core/__pycache__/profiler.cpython-312.pyc | Bin 0 -> 1787 bytes .../__pycache__/rabbit_mq.cpython-312.pyc | Bin 0 -> 2840 bytes .../core/__pycache__/redis.cpython-312.pyc | Bin 0 -> 1715 bytes backend/app/core/app.py | 8 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 1371 bytes .../config/__pycache__/base.cpython-312.pyc | Bin 0 -> 1891 bytes .../__pycache__/components.cpython-312.pyc | Bin 0 -> 10009 bytes .../config/__pycache__/local.cpython-312.pyc | Bin 0 -> 1536 bytes .../__pycache__/production.cpython-312.pyc | Bin 0 -> 1531 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 517 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 3199 bytes .../handlers/__pycache__/auth.cpython-312.pyc | Bin 0 -> 1491 bytes .../__pycache__/database.cpython-312.pyc | Bin 0 -> 2784 bytes .../handlers/__pycache__/http.cpython-312.pyc | Bin 0 -> 2579 bytes .../__pycache__/other.cpython-312.pyc | Bin 0 -> 2796 bytes .../__pycache__/utils.cpython-312.pyc | Bin 0 -> 2757 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 677 bytes .../types/__pycache__/base.cpython-312.pyc | Bin 0 -> 1339 bytes .../types/__pycache__/config.cpython-312.pyc | Bin 0 -> 1086 bytes .../__pycache__/database.cpython-312.pyc | Bin 0 -> 2312 bytes .../types/__pycache__/token.cpython-312.pyc | Bin 0 -> 2111 bytes .../types/__pycache__/user.cpython-312.pyc | Bin 0 -> 2160 bytes .../__pycache__/validation.cpython-312.pyc | Bin 0 -> 3306 bytes backend/app/core/exceptions/types/user.py | 8 + .../app/core/exceptions/types/validation.py | 24 ++ backend/app/main.py | 112 ++++--- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 178 bytes .../models/__pycache__/base.cpython-312.pyc | Bin 0 -> 260 bytes .../__pycache__/permission.cpython-312.pyc | Bin 0 -> 3138 bytes .../domain/__pycache__/role.cpython-312.pyc | Bin 0 -> 3106 bytes .../__pycache__/session.cpython-312.pyc | Bin 0 -> 1554 bytes .../domain/__pycache__/user.cpython-312.pyc | Bin 0 -> 4620 bytes backend/app/models/domain/permission.py | 74 +++++ backend/app/models/domain/role.py | 19 ++ backend/app/models/domain/user.py | 39 +++ .../schemas/__pycache__/email.cpython-312.pyc | Bin 0 -> 518 bytes .../__pycache__/pagination.cpython-312.pyc | Bin 0 -> 720 bytes .../__pycache__/password.cpython-312.pyc | Bin 0 -> 1794 bytes .../__pycache__/response.cpython-312.pyc | Bin 0 -> 1713 bytes .../schemas/__pycache__/role.cpython-312.pyc | Bin 0 -> 7528 bytes .../__pycache__/session.cpython-312.pyc | Bin 0 -> 1444 bytes .../schemas/__pycache__/token.cpython-312.pyc | Bin 0 -> 980 bytes .../schemas/__pycache__/user.cpython-312.pyc | Bin 0 -> 7490 bytes backend/app/models/schemas/role.py | 150 ++++++++- .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 184 bytes .../__pycache__/base.cpython-312.pyc | Bin 0 -> 6988 bytes .../__pycache__/permission.cpython-312.pyc | Bin 0 -> 8099 bytes .../__pycache__/role.cpython-312.pyc | Bin 0 -> 15460 bytes .../__pycache__/session.cpython-312.pyc | Bin 0 -> 6270 bytes .../__pycache__/unit_of_work.cpython-312.pyc | Bin 0 -> 4459 bytes .../__pycache__/user.cpython-312.pyc | Bin 0 -> 10347 bytes backend/app/repositories/permission.py | 137 +++++++++ backend/app/repositories/role.py | 250 ++++++++++++++- backend/app/repositories/unit_of_work.py | 2 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 180 bytes .../__pycache__/auth_service.cpython-312.pyc | Bin 0 -> 12798 bytes .../__pycache__/user_service.cpython-312.pyc | Bin 0 -> 9207 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 177 bytes .../utils/__pycache__/auth.cpython-312.pyc | Bin 0 -> 4182 bytes .../utils/__pycache__/cache.cpython-312.pyc | Bin 0 -> 4103 bytes .../__pycache__/notification.cpython-312.pyc | Bin 0 -> 2948 bytes .../__pycache__/password.cpython-312.pyc | Bin 0 -> 2147 bytes .../__pycache__/response.cpython-312.pyc | Bin 0 -> 1780 bytes .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 182 bytes .../__pycache__/user.cpython-312.pyc | Bin 0 -> 3297 bytes frontend/src/components/Header.tsx | 51 ++- frontend/src/components/RoleDetail.tsx | 291 ++++++++++++++++++ frontend/src/components/RoleList.tsx | 205 ++++++++++++ frontend/src/components/RoleManagement.tsx | 127 ++++++++ frontend/src/redux/services/api.ts | 2 +- frontend/src/redux/services/roleApi.ts | 210 +++++++++++++ frontend/src/routes/routes.tsx | 5 + frontend/src/types/role.ts | 57 ++++ frontend/src/types/user.ts | 4 +- 95 files changed, 2196 insertions(+), 76 deletions(-) create mode 100644 backend/alembic/__pycache__/env.cpython-312.pyc create mode 100644 backend/alembic/versions/__pycache__/09fac9965b5b_init_tables.cpython-312.pyc create mode 100644 backend/alembic/versions/__pycache__/a1b2c3d4e5f6_add_permissions_tables.cpython-312.pyc create mode 100644 backend/alembic/versions/a1b2c3d4e5f6_add_permissions_tables.py create mode 100644 backend/app/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/__pycache__/main.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/deps.cpython-312.pyc create mode 100644 backend/app/api/__pycache__/routers.cpython-312.pyc create mode 100644 backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc create mode 100644 backend/app/api/endpoints/__pycache__/permissions.cpython-312.pyc create mode 100644 backend/app/api/endpoints/__pycache__/roles.cpython-312.pyc create mode 100644 backend/app/api/endpoints/__pycache__/users.cpython-312.pyc create mode 100644 backend/app/api/endpoints/permissions.py create mode 100644 backend/app/api/endpoints/roles.py create mode 100644 backend/app/core/__pycache__/app.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/db.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/events.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/limiter.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/middleware.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/profiler.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/rabbit_mq.cpython-312.pyc create mode 100644 backend/app/core/__pycache__/redis.cpython-312.pyc create mode 100644 backend/app/core/config/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/core/config/__pycache__/base.cpython-312.pyc create mode 100644 backend/app/core/config/__pycache__/components.cpython-312.pyc create mode 100644 backend/app/core/config/__pycache__/local.cpython-312.pyc create mode 100644 backend/app/core/config/__pycache__/production.cpython-312.pyc create mode 100644 backend/app/core/exceptions/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/auth.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/database.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/http.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/other.cpython-312.pyc create mode 100644 backend/app/core/exceptions/handlers/__pycache__/utils.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/base.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/config.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/database.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/token.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/user.cpython-312.pyc create mode 100644 backend/app/core/exceptions/types/__pycache__/validation.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/models/__pycache__/base.cpython-312.pyc create mode 100644 backend/app/models/domain/__pycache__/permission.cpython-312.pyc create mode 100644 backend/app/models/domain/__pycache__/role.cpython-312.pyc create mode 100644 backend/app/models/domain/__pycache__/session.cpython-312.pyc create mode 100644 backend/app/models/domain/__pycache__/user.cpython-312.pyc create mode 100644 backend/app/models/domain/permission.py create mode 100644 backend/app/models/schemas/__pycache__/email.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/pagination.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/password.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/response.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/role.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/session.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/token.cpython-312.pyc create mode 100644 backend/app/models/schemas/__pycache__/user.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/base.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/permission.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/role.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/session.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/unit_of_work.cpython-312.pyc create mode 100644 backend/app/repositories/__pycache__/user.cpython-312.pyc create mode 100644 backend/app/repositories/permission.py create mode 100644 backend/app/services/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/services/__pycache__/auth_service.cpython-312.pyc create mode 100644 backend/app/services/__pycache__/user_service.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/auth.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/cache.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/notification.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/password.cpython-312.pyc create mode 100644 backend/app/utils/__pycache__/response.cpython-312.pyc create mode 100644 backend/app/validators/__pycache__/__init__.cpython-312.pyc create mode 100644 backend/app/validators/__pycache__/user.cpython-312.pyc create mode 100644 frontend/src/components/RoleDetail.tsx create mode 100644 frontend/src/components/RoleList.tsx create mode 100644 frontend/src/components/RoleManagement.tsx create mode 100644 frontend/src/redux/services/roleApi.ts create mode 100644 frontend/src/types/role.ts diff --git a/backend/alembic/__pycache__/env.cpython-312.pyc b/backend/alembic/__pycache__/env.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c30fe800e36d2365a4fb33480e751664e35aed3 GIT binary patch literal 3807 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 literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/__pycache__/09fac9965b5b_init_tables.cpython-312.pyc b/backend/alembic/versions/__pycache__/09fac9965b5b_init_tables.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e514833607b0e77f46871e6992c7afece6128f2 GIT binary patch literal 4875 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 literal 0 HcmV?d00001 diff --git a/backend/alembic/versions/a1b2c3d4e5f6_add_permissions_tables.py b/backend/alembic/versions/a1b2c3d4e5f6_add_permissions_tables.py new file mode 100644 index 00000000..e648fbcd --- /dev/null +++ b/backend/alembic/versions/a1b2c3d4e5f6_add_permissions_tables.py @@ -0,0 +1,53 @@ +"""add permissions and role_permissions tables + +Revision ID: a1b2c3d4e5f6 +Revises: 09fac9965b5b +Create Date: 2026-04-15 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a1b2c3d4e5f6' +down_revision: Union[str, None] = '09fac9965b5b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('permissions', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=50), nullable=False), + sa.Column('description', sa.String(length=255), nullable=True), + sa.Column('resource', sa.String(length=30), nullable=False), + sa.Column('action', sa.String(length=20), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_permissions_action'), 'permissions', ['action'], unique=False) + op.create_index(op.f('ix_permissions_name'), 'permissions', ['name'], unique=True) + op.create_index(op.f('ix_permissions_resource'), 'permissions', ['resource'], unique=False) + + op.create_table('role_permissions', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.PrimaryKeyConstraint('role_id', 'permission_id'), + sa.UniqueConstraint('role_id', 'permission_id', name='uq_role_permission') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('role_permissions') + op.drop_index(op.f('ix_permissions_resource'), table_name='permissions') + op.drop_index(op.f('ix_permissions_name'), table_name='permissions') + op.drop_index(op.f('ix_permissions_action'), table_name='permissions') + op.drop_table('permissions') + # ### end Alembic commands ### diff --git a/backend/app/__pycache__/__init__.cpython-312.pyc b/backend/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6003263b4cd7b33a09e2940d2ed26d9d4b928bdd GIT binary patch literal 171 zcmX@j%ge<81V%sZWrFC(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd>JB7atYTbJi?d7e z3u2z^Xm~ky(bL}M=kqtmq?P966qh6>XX~aV0_lQG-J;aQ
qpipXFN=#xwK}>vn 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 V3y!^c$KJ0TeWgKgP=KX{{{c1H=$Zfk literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/deps.cpython-312.pyc b/backend/app/api/__pycache__/deps.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bd74f90d6606bb23c5b6cede172b69a05732694e GIT binary patch literal 9055 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 literal 0 HcmV?d00001 diff --git a/backend/app/api/__pycache__/routers.cpython-312.pyc b/backend/app/api/__pycache__/routers.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cdc3b4f05016bc7240f8e1f50b1cb19605b18a7 GIT binary patch literal 987 zcmbtT&ubGw6n?YWU)hj|7d1h|Lp_9~1#cq8Q0PG|N=&urx^#)zN!)aQF}s1(Q^BL? zReO+tAVlh+;3a>8;KiU48Bi3J(wkCx^WaRHHJ1oV2ll=9eSGiZ&FswUbXo(pI=?>I z#|40|9K=f+7rPG;z$36g2Mbw(gIqxu5U&$X!WDJVm2}CKb=g&PWyFh)>L&Fh0s*kF zIkwR?thJ8D=97%Y*0BkSD@$r3oNVP{JjpNQ&BQ)J@E1pAs-toMM|rBFdH_dhs$=qB z9M`mb>TX7xuUtGy{C0#%IH&FKBgrG0WNdXr=0z&j+R+`F3NUf)FtmLyq!RHR%u+jK zZA`C*IErkq8B%eciSq&#qZ&hUKybtEQ0Y1sQmHy>OI0RTSMnLEn5N~|O_M5(S{T&= zoA6&l57&ZVj-fg1S%GhRQAk9_6MmzJ%!9&Zhq$cn6U>6wu$x5Xti~G6{e97l?bV&O zg<~v_dA@gZ`Lwxot@<2xSr_bGVqxK|v4F$X$PbKf{q>zEPrpC7x7~edG}?~C#;dQ+ zj>nuOxK@vh+l*o(8f*^s*+zVt!5UT1xxUqQ@Oi?oAbS81K4alW0wMHM5Rka10y?n` zrMFP}2s2;ONABNR*FJ+bgzNyak7v4tjY4m^ccFh{SgH(4l{b)m2UkD*5uEB4H;TPx r?`FRPWEkAA-U#R~+PuTbH-Wug(k?R|bnK%;P User: + if not current_user.has_permission(permission_name): + logger.warning( + f"User {current_user.id} attempted action without permission: {permission_name}" + ) + raise UserAccessError(error_type="insufficient_permissions") + return current_user + + return permission_checker + + +def require_any_permission(permission_names: list[str]): + """ + Dependency factory for checking if a user has any of the specified permissions. + Admin users have all permissions. + """ + + async def permission_checker( + current_user: Annotated[User, Depends(get_current_user)], + ) -> User: + if not current_user.has_any_permission(permission_names): + logger.warning( + f"User {current_user.id} attempted action without any of permissions: {permission_names}" + ) + raise UserAccessError(error_type="insufficient_permissions") + return current_user + + return permission_checker + + +def require_all_permissions(permission_names: list[str]): + """ + Dependency factory for checking if a user has all of the specified permissions. + Admin users have all permissions. + """ + + async def permission_checker( + current_user: Annotated[User, Depends(get_current_user)], + ) -> User: + if not current_user.has_all_permissions(permission_names): + logger.warning( + f"User {current_user.id} attempted action without all permissions: {permission_names}" + ) + raise UserAccessError(error_type="insufficient_permissions") + return current_user + + return permission_checker + + +def require_role(role_name: str): + """ + Dependency factory for checking if a user has a specific role. + """ + + async def role_checker( + current_user: Annotated[User, Depends(get_current_user)], + ) -> User: + if current_user.role is None or current_user.role.name != role_name: + logger.warning( + f"User {current_user.id} attempted action without role: {role_name}" + ) + raise UserAccessError(error_type="insufficient_permissions") + return current_user + + return role_checker + + async def pagination_params(skip: int = 0, limit: int = 100) -> PaginationParams: """ Get pagination parameters. diff --git a/backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2db89bc4cfc5a81475e3747f34055cbbda22014b GIT binary patch literal 4853 zcmbtXO>7&-6`mn?`AT+EZ^W&@Tu?3?ozZZ z3P!uY&b*y@^WK~9{mks2!r=gc=g^mb%|8wi@(=uQp0G`9&bSG=O;n-~m8z}+)m(~8 zb1QC5QY1}QWQybNf=BZzUO`I*pXOKmT0jYiZ@CcELP|)`os5NSKBZ6VSNbV&5#~~ZYG{ex4c~ED z_A0yC?&X)AO>FgQ8|!8A^32wVL)~q^13SbAfjC$gNI8$Xr)lj-3mf9j+UMT)5bJ!(PA}|xcuQ9U z?|m(F?saskBP{lWtbHs1{`2NS>i#Wx%KjE|MjLX*ShR(lM3bD+>*I-u)&9WAmC6-X ztmLzqO1@N#v!Yrq<%<+{Zd!ez9f=nIU$k(@$LHDV4WL^wutTw*+*vn_w6q%u${FO;sbqR}S0g;I*;IMc6y zJRizEl>v+WNWE5C$`_3gk}0;7*I{?7@>B*aHYBtgM0v`b;r?P~zndXwEt#go@HIkJuO<&MGUsn3bBs z8592&yql+>`5K{6=x(@|?KE%VBR9xh>bU+EQC;7Gl|OXdc7Iov$p!KPc~APe`x;HS zR}Tc@@Hm@|XW~V6Esi$E^TjyCKwQ5LC}>H+mY}>3w9JD;!raCujm!y2Oz?h~<-4&N zfJR5tQT%_-19AGL#nVi`S}Bzm|M}_7FFyF_U*Es=`A>emm|H0n0IKZO$y^5da(37E&F0?GZF_}3M7Ubss+6AfmSp(QImL{|3}4yK z8sQ!zn1c*syv!k~=OLSAfdStK*6{t+WWd&;u zR7#Begxf+7;S|WvL#2S+5v$XJ(}6mHEIUDsB}1&^2Y~e$y!wmKSa8Q0F<#15 z^~m%_aJni@SA)}!`7rRVwnI?Cfivl(hY_;ta{8G%fO#o^xCF$GF}8h#QRn^y@kLt{ zWY?;d*>lBwC7-Do{n3>ytm`>DDq)*8Qy1LP1GWZwSD(`e>3>2!QTU{ za~r|Asx(&(&e^aIjnj|mnDmJ}PFKTIa58U&Y1nbN6{pRxHqNUeDE*&c-TuUCKABpB zJtV-sz$*IIRl>2=iC(wSfq@4bY6PDVdg3c)JLw4?03NH}^8o)6@?lekS3eF-b37+& zk!0N$uKRXv_=an~VL^=7eB*Ur`=)=p<4Bas1Jm>u^sw|xd792A#td&dt(LOswBfZc zE*wiG$1=gu9~>QFMC{2VB%8P%>fDRtA-LBh;a-zeSy|^k_Tvsa)l=t;*D9DkQN9{G1RY;pSU8tH^p%6@S5mVl7iLczyHe1l zUzd)Yu)C=Z32%s!@a~RqNIzmB$7xt^k-xfrirNghHnNozfxc) z_&ki^Jy`z(oC~I#Qu@Hj!J8h_LtMV^^}p5s?l(Vt^V3Tk#}{hH7jE{i(~Ezho_EJK z0%NtnSk<3c>#lkauG2%N%TJHj!@Z^(`p?2KQ-&Udw0E0cqAfCX(AW)lb z;yR4YC2C4I)8nXo{X>ST4zyC$Mm_aFr|E`XsDj>Bg_NZVDNq%B7zC=JYSNT&V%X87 ztjVY;X}ZEFv&VEpFJwY*%S6hOiIgQ1D3m!R)|qh>*Y{T6yL0Qb%XE3^(K;aDhW@i? z+?1gQQJ%JkUZ8MUx8N~jwiVUz;5yyYYO{WtqV?#01U5x&WK*;OEszejK!SBq1UE$+ zTaF{RFx5c!TkCYU=?c(SX}x`q=|)0`MZy*XI1_c>H4Phiv!+<5o>~jE3 ouTvOn2QWJaO*iyHH1xJ)q%6TmS#puG!~%s<2OppWeij1yzjWebf&c&j literal 0 HcmV?d00001 diff --git a/backend/app/api/endpoints/__pycache__/permissions.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/permissions.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5ab1523da0f2ecf74c03551912e72f71da722d8 GIT binary patch literal 6319 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 Va2xht76U)`nR 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 literal 0 HcmV?d00001 diff --git a/backend/app/api/endpoints/__pycache__/users.cpython-312.pyc b/backend/app/api/endpoints/__pycache__/users.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cfc09b6bfe41037d3d7e0630315e7ce38c2a36d GIT binary patch literal 6527 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 list[PermissionPublic]: + """ + Get all permissions with pagination. + Requires authentication. + """ + permissions = await uow.permissions.get_all(session=uow.session) + return [PermissionPublic.model_validate(perm) for perm in permissions] + + +@router.get("/by-resource/{resource}", response_model=list[PermissionPublic]) +async def get_permissions_by_resource( + current_user: CurrentUser, + resource: str, + uow: UnitOfWorkDep, +) -> list[PermissionPublic]: + """ + Get permissions by resource. + Requires authentication. + """ + permissions = await uow.permissions.get_by_resource(session=uow.session, resource=resource) + return [PermissionPublic.model_validate(perm) for perm in permissions] + + +@router.get("/{permission_id}", response_model=PermissionPublic) +async def get_permission_by_id( + current_user: CurrentUser, + permission_id: int, + uow: UnitOfWorkDep, +) -> PermissionPublic: + """ + Get permission by ID. + Requires authentication. + """ + permission = await uow.permissions.get(session=uow.session, id=permission_id) + if not permission: + raise ValidationError("permission_not_found") + return PermissionPublic.model_validate(permission) + + +@router.post("/", response_model=PermissionPublic, status_code=status.HTTP_201_CREATED) +async def create_permission( + current_superuser: CurrentSuperUser, + permission_in: PermissionCreate, + uow: UnitOfWorkDep, +) -> PermissionPublic: + """ + Create a new permission. + Requires admin privileges. + """ + existing_permission = await uow.permissions.get_by_name( + session=uow.session, + name=permission_in.name, + ) + if existing_permission: + raise ValidationError("permission_exists") + + new_permission = await uow.permissions.create( + session=uow.session, + obj_in=permission_in, + ) + return PermissionPublic.model_validate(new_permission) + + +@router.patch("/{permission_id}", response_model=PermissionPublic) +async def update_permission( + current_superuser: CurrentSuperUser, + permission_id: int, + permission_in: PermissionUpdate, + uow: UnitOfWorkDep, +) -> PermissionPublic: + """ + Update a permission. + Requires admin privileges. + """ + permission = await uow.permissions.get(session=uow.session, id=permission_id) + if not permission: + raise ValidationError("permission_not_found") + + if permission_in.name and permission_in.name != permission.name: + existing_permission = await uow.permissions.get_by_name( + session=uow.session, + name=permission_in.name, + ) + if existing_permission: + raise ValidationError("permission_exists") + + updated_permission = await uow.permissions.update( + session=uow.session, + db_obj=permission, + obj_in=permission_in, + ) + return PermissionPublic.model_validate(updated_permission) + + +@router.delete("/{permission_id}") +async def delete_permission( + current_superuser: CurrentSuperUser, + permission_id: int, + uow: UnitOfWorkDep, + request: Request, +) -> JSONResponse: + """ + Delete a permission. + Requires admin privileges. + """ + permission = await uow.permissions.get(session=uow.session, id=permission_id) + if not permission: + raise ValidationError("permission_not_found") + + await uow.permissions.delete(session=uow.session, obj=permission) + + return create_response( + status_code=status.HTTP_200_OK, + message=f"Permission {permission.name} deleted successfully", + request=request, + ) diff --git a/backend/app/api/endpoints/roles.py b/backend/app/api/endpoints/roles.py new file mode 100644 index 00000000..dafae65d --- /dev/null +++ b/backend/app/api/endpoints/roles.py @@ -0,0 +1,220 @@ +""" +Role management endpoints. +""" + +from fastapi import APIRouter, Depends, Request, status +from fastapi.responses import JSONResponse + +from app.api.deps import ( + CurrentSuperUser, + CurrentUser, + PaginationDep, + UnitOfWorkDep, +) +from app.core.exceptions import UserAccessError, ValidationError +from app.models.schemas.role import ( + AssignPermissionsRequest, + RoleCreate, + RolePublic, + RoleUpdate, + RoleWithUsers, +) +from app.utils.response import create_response + +router = APIRouter(prefix="/roles", tags=["Roles"]) + + +@router.get("/", response_model=list[RolePublic]) +async def get_roles( + current_user: CurrentUser, + uow: UnitOfWorkDep, + pagination: PaginationDep, +) -> list[RolePublic]: + """ + Get all roles with pagination. + Requires authentication. + """ + roles = await uow.roles.get_all_with_permissions(session=uow.session) + return [RolePublic.model_validate(role) for role in roles] + + +@router.get("/with-users", response_model=list[RoleWithUsers]) +async def get_roles_with_user_count( + current_superuser: CurrentSuperUser, + uow: UnitOfWorkDep, +) -> list[RoleWithUsers]: + """ + Get all roles with user count. + Requires admin privileges. + """ + roles = await uow.roles.get_all_with_permissions(session=uow.session) + result = [] + for role in roles: + user_count = await uow.roles.get_role_user_count(session=uow.session, role_id=role.id) + role_public = RolePublic.model_validate(role) + role_with_users = RoleWithUsers( + id=role_public.id, + name=role_public.name, + description=role_public.description, + permissions=role_public.permissions, + user_count=user_count, + ) + result.append(role_with_users) + return result + + +@router.get("/{role_id}", response_model=RolePublic) +async def get_role_by_id( + current_user: CurrentUser, + role_id: int, + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Get role by ID. + Requires authentication. + """ + role = await uow.roles.get_by_id_with_permissions(session=uow.session, role_id=role_id) + if not role: + raise ValidationError("role_not_found") + return RolePublic.model_validate(role) + + +@router.post("/", response_model=RolePublic, status_code=status.HTTP_201_CREATED) +async def create_role( + current_superuser: CurrentSuperUser, + role_in: RoleCreate, + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Create a new role. + Requires admin privileges. + """ + existing_role = await uow.roles.get_by_name(session=uow.session, name=role_in.name) + if existing_role: + raise ValidationError("role_exists") + + new_role = await uow.roles.create_role_with_permissions( + session=uow.session, + obj_in=role_in, + ) + return RolePublic.model_validate(new_role) + + +@router.patch("/{role_id}", response_model=RolePublic) +async def update_role( + current_superuser: CurrentSuperUser, + role_id: int, + role_in: RoleUpdate, + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Update a role. + Requires admin privileges. + """ + role = await uow.roles.get_by_id_with_permissions(session=uow.session, role_id=role_id) + if not role: + raise ValidationError("role_not_found") + + if role_in.name and role_in.name != role.name: + existing_role = await uow.roles.get_by_name(session=uow.session, name=role_in.name) + if existing_role: + raise ValidationError("role_exists") + + updated_role = await uow.roles.update_role_with_permissions( + session=uow.session, + db_obj=role, + obj_in=role_in, + ) + return RolePublic.model_validate(updated_role) + + +@router.delete("/{role_id}") +async def delete_role( + current_superuser: CurrentSuperUser, + role_id: int, + uow: UnitOfWorkDep, + request: Request, +) -> JSONResponse: + """ + Delete a role. + Requires admin privileges. + Cannot delete roles that have users assigned. + """ + role = await uow.roles.get(session=uow.session, id=role_id) + if not role: + raise ValidationError("role_not_found") + + user_count = await uow.roles.get_role_user_count(session=uow.session, role_id=role_id) + if user_count > 0: + raise UserAccessError("role_has_users") + + await uow.roles.delete(session=uow.session, obj=role) + + return create_response( + status_code=status.HTTP_200_OK, + message=f"Role {role.name} deleted successfully", + request=request, + ) + + +@router.post("/{role_id}/permissions", response_model=RolePublic) +async def assign_permissions_to_role( + current_superuser: CurrentSuperUser, + role_id: int, + permission_ids: list[int], + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Assign permissions to a role. + Requires admin privileges. + """ + role = await uow.roles.assign_permissions_to_role( + session=uow.session, + role_id=role_id, + permission_ids=permission_ids, + ) + if not role: + raise ValidationError("role_not_found") + return RolePublic.model_validate(role) + + +@router.post("/{role_id}/permissions/{permission_id}", response_model=RolePublic) +async def add_permission_to_role( + current_superuser: CurrentSuperUser, + role_id: int, + permission_id: int, + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Add a single permission to a role. + Requires admin privileges. + """ + role = await uow.roles.add_permission_to_role( + session=uow.session, + role_id=role_id, + permission_id=permission_id, + ) + if not role: + raise ValidationError("role_or_permission_not_found") + return RolePublic.model_validate(role) + + +@router.delete("/{role_id}/permissions/{permission_id}", response_model=RolePublic) +async def remove_permission_from_role( + current_superuser: CurrentSuperUser, + role_id: int, + permission_id: int, + uow: UnitOfWorkDep, +) -> RolePublic: + """ + Remove a permission from a role. + Requires admin privileges. + """ + role = await uow.roles.remove_permission_from_role( + session=uow.session, + role_id=role_id, + permission_id=permission_id, + ) + if not role: + raise ValidationError("role_or_permission_not_found") + return RolePublic.model_validate(role) diff --git a/backend/app/api/routers.py b/backend/app/api/routers.py index 3e7b1635..d97633fd 100644 --- a/backend/app/api/routers.py +++ b/backend/app/api/routers.py @@ -4,10 +4,12 @@ from fastapi import APIRouter -from app.api.endpoints import auth, users +from app.api.endpoints import auth, permissions, roles, users from app.core.config import settings api_router = APIRouter() api_router.include_router(auth.router, tags=["Auth"], prefix=settings.API_V1_STR) api_router.include_router(users.router, tags=["Users"], prefix=settings.API_V1_STR) +api_router.include_router(roles.router, tags=["Roles"], prefix=settings.API_V1_STR) +api_router.include_router(permissions.router, tags=["Permissions"], prefix=settings.API_V1_STR) diff --git a/backend/app/core/__pycache__/app.cpython-312.pyc b/backend/app/core/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b11a4814af60ed77526e3cb246e5736631fb16e9 GIT binary patch literal 1681 zcmaJ>%}*Og6rWvty~ZDFn*MA8A#8D^a zswFk_6({a?mAXPd<|N!?DG9!cK)#E74L4XL0VKgu97M)QUtD z677TY%d*foTT?+_AZj^^FMbg#rMI(%p4w32j>&?_g}b_0tva@42Db0%7A2umOgtAi zDM&m%WjcaS zD>3H89UQw%Sk?4s7AnJIlmwg9vqYJMNHF3sA~22xdVm5k7fK*^yp3qbJG19jJN!9Y zM1hDJ%DL}C26pCK_FG`rBY6iskskvqg?uWV+mUL`o!4}1N7~4opK1AlxL_#%zaK|% zv?7H=sYp*^kK5TxXh&K?1+_Mk&_SjIr-{ev8_f3#3ReO< za0pjF22KbDq}uC7U;>4b9kfZE@GX|PG+NU>8ebvQF1MKsQPMzMf6D;QRBVs&Tnhrj zF%ZQz9oG56CBQ6v3fGB=;b5+j`&FM2qm2t)RNRk>=isS@#rc`(sbynsa(0?$I%s8j zap~^-98Zfx4Tx)EGcb7q+RRva(^y(wk<4|-fSY3lD z;^9>YvkHe2;S{UeJP|fJF#ysLi2FJW_6ew6bfTeT_o3E%pk1zOmk+a-4zkzk*=vVA z!-x5ygZyYcKf2$0{fBq=FTUN#^_?VR$=L4HNfyPk2de&ysyE`vgLr>E-hY_tKkOeq z=)Y0#ztI>RZVU|WjkMHjWBUW+%{1y7I6-PxqS=j7ncbOVI35)=t{MjREyLg$%Xb`N ziJt%~nrjvny~Sg}b`^dlR5TJ5Oy_F*&f)v;d>a+dN?H{44(rM@q5z^2puDnO37?Y= ziCCfzKO%Zn_*X>S_&ctTHy!dGEdmtH%)SKGj7XC7LYAfI%Q%uoen;1j(7>aB!1j^+b`KzY@R&Jew!A5?r1qO0Cc{kl0LwBSZ=^v#}%KV}A?x|mI j?+@Sn$*K=e?B^%y+O4O!uD#zB%gq>yrB77w!esvgW2VZg literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/db.cpython-312.pyc b/backend/app/core/__pycache__/db.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..820c1b6df0ec1b75fb58c5f59fc07c13e5e9f62d GIT binary patch literal 7919 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 Rk52-3`fi@w#3WhV{{j2<#P9$B literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/events.cpython-312.pyc b/backend/app/core/__pycache__/events.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb41107f034c7298f14783ae31e39bcd1cfe70f6 GIT binary patch literal 2819 zcmb7GUu+ab7@yhQyPMngw(a!~w1xKCo`s`wL>?p|LIQVHs#pv(Q7%o6?d}w=?Czbr zTS_Y?1c-*H0e#RWY6KqygCr)t_~1(^Z@%=nhm#Gci4f)Oppc;P!EfgNPzo5BWWM>$ zH{bWo%(vfs-~Jp5$q3rU`@d#Z`VjhqO{&db0gD|Nq0f+xQb@^737BEg5KSp1 zaV!}=)1UHl>@xx;NfD0yM$nW~GRFa<&a6+>o1s+5R8k7(5u?FuOf_;GG@8tCDr`2V zn)zHdTFll|D@FpM_0U7KapL*v@g9s9;C!rsgaQ)r@@y%sN_<1aq8q$D?$%Z%Z(gR zE#08D6PIr>%Q5VQwmH+5Hsn%Ub+a}wnpMWovX)B^yQXTX`zd1wURE7<+Z#gw>$%Dv z>NuIK1#`bc4XU{@(ev5VwWqzX<7RVdbsUVQRoA6v&h_LeHRGnoGl!`TD#W3#o3Zvg zF~O7dv2p{@d8eHbI%VOPTJP6R`E1<7sLxX z4t;S1$ZH5gj2uA|OC`7%LatZ=5Hr;;Q5}yXU3dr9KNUU`-}Cvv%CxWvedxO=PT`n1 z6O|*-5(lVuFp^YVbxd_=B$2f&a0GZI&X;eoJ%{1!0eYBfli-d3Fuuih6ATVpjNE$_ zXmaytk~#I{Lim`7#*K9498WqgX_n^`D!k+-lnSNj*bC~TKcG( z%Z+MTn~w62>%?=@9?{s5z=QTmFdSz4bOHGb-9L&-ji@esYQxD5U-X}8`aD*UqeUrt zMT-98)Pu;euC@3K?w8K`*W!oV$(+R9`67U_JKdTykDw(E!^<3oYYr1;`nX4i;PYlw zBQrxEc#XNQ?62V%wnMOq-3FxWAA1ErFIWx>5VSjBzM3sF8Gfz11g>>@O+oG`N*!0E zj>>f!+VRJDh4hKP9eW~p>Y+Qa5E0?UE$#!d{7w^`u@qrx_N^kx2O+5u;KL>^|T|p*uH#l%J7| z8rq9We_rV?C~J?tQWAo`zH3U`>8XO!T_oKLr2CrEQB=C~N_Ro&DUzNA(sNB&F&ii- zks^sKkjO7e>nZD`RZzN$q-%k6T_d62fn}aFKgbrz>IJfzTjv2Mn=HfwyH#LLeB^f6g zrAu|44EN(iSh}=^;d!voBF&3Fh6x7wd5{EJ&#A&`BW9~%PFpx_Ye^1>^XqV8J((X^ z1^99RCuMS(a2&1H9$&@v*H898qe&vS=#dIYFwy5ESz~h_mmo&7xi*HuTE}HCw2`F4<<&2 zb`7U@5032Kn|^)UTj`PALxX#~$m5pZ+%`0l-ZKbl$vx$#!Ri5z=5Ns=KvW6zm&~j_ zY0$0qM&Pkm?3@Eq5;4ZN1p!O<2*T^Gqjfh?=XJEy$zS%$t!EomF@1yzXraKHTim-T_E!r=@S^onGI)GjP literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/limiter.cpython-312.pyc b/backend/app/core/__pycache__/limiter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c6e9be1f720e2827c99d4f559f2ae3162558830 GIT binary patch literal 2377 zcmb7FU2Gdg5Z=49@6P{8>e7gK(7{B=u`jIWX5$H1OqZ~pPk&bdm=XBoWavV^>&zdza-{Fm-m}eNzTbGFPec;Fc)-r$qbdkxv-+)IBcK`FD(mo|iGt?f-87d)(4_vY z(PL-ej8A*62pwH+C*D~c`L|u}UiX&0I0!ct=S95_ZrZ=VC%4sh2h*B^71Jmgj!`Zs z^VM?RF|4v?8qPv0c#AokPV!Vr<5#QLcBp7Onp3rt91XvanVC*wyJD4X3~T8abAk!5 zz09e*oH&=qSjRdD`wG}m2`*UpaNqOJ4)U(aM@9&W|eLbQ?9BOJ2`a3B4;%M(PkXw z4iX-&Tnn5WN@L9&nwm}esdO4^Iwm$1ARE+gS0K+o9AZ?64Ax13u(G3(dC!*$9LtSn zsz$|T5r&KWeiZ2V=-fEA&p1|P?x(Nnw=RGD^Luap^3fM_^HtLXl;qD0&1*m_#t`hz zJ9Dpq2$%Iatx}oGTLjO!sKX>xS)fsM3p(H~+rfVye6|5o9kpe&{l1Gc@6WX2iDo>} zccPU@JD_G*zFxCPmj`_gq*yzx3{F4SDxZptD*f3-~M$t4Ao?Ub;BM#bck}JlsiO& z6=LaCw=9wZmCxV>m6Zt%*^WiD0#>WUpb{=?ub5b;0zQ||v%q_ULFrBS*1v)2DB=LV z&fn19O!E@&{Jrb05akwmXc9a?U-VpW(jrUV9RsX|%YF|=9CAXQffM$)V$|a{sqWML zy7(clOS)`up9Z}4l0lydXSrmsw%T`V=CpXvI7-GSR|6Ni##ftR-pj) z64X-}x)T+u#H2oUQNr2>@xesC$fHq)&mvWwXT zIyJkcen_0j`?kEc8#%&$0POeR0{>U^M;&d*sCUn$v#ULc`qXt_@a^f>r`LjmD~DGO zeE#y)9p4UoJ=8{IctCX7+o3LV_VNY0f4AX5pGzLu}#hL<4<$0q(SR=*RF z4K#QI4ctHnZ=eG|qkT66{qO8(1^SzT{!34+2KLp(wixCTmnPc?$cnJe$nD)I)VCHH zSnC6$)=}GpsBh sYleq6BwvWHOYJZU^jwIXkJSBbv4<0v3vC3X@#ut0zdN|WmhP+m0(qEJUH||9 literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/middleware.cpython-312.pyc b/backend/app/core/__pycache__/middleware.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d5856868b6725deb45986bd54bf1c2915f5086e GIT binary patch literal 5444 zcmcgwZ)_aJ6`#HRzrJ%E+ll|%YsZec#CAfQlEi_~I1Wja;L-pETqmo`?bzP)`OeJl zVf#3@2vFrnQ~@D1ww%ZgUjhn920kPofeHz!U+(HizHXy7RjDW+DqU(LLE8^~vv+%U zj;<*E(2hN3IzOo6Bwzyu!5=`7}T4htiK&cDtLi4IS|lFsQO@Fd{L4o}8{PWcK}oH0c& zgR!z1(eJgZa*CZAC}5gle^ZE5!8<(#2o#e1kzhBc<@*a{e7 zOy$BsP2`TpS!Ihk1#LMP2#2)@p0;d(U@Jxq?yE5-8*=8px3eCc)kS zJ!xT(3|BqjNigpIuYoJP3>yE6z0CfC=g}a6(RX=CMGI8@`xDYQDZbZw#UQj19E zV5Hvd7urx`fp)1ybd__?tc0Y(EhsI|wR!^lZ;{i5tDd42Vt!N%HM_eN+K^hHT}l_P z2=7aU(|r|Ab5{JBIK@n%Y4!y)#eBd{F@8DF4%bbg-bKq(BpSq~iA@Kla(CWwBXHPa z-z)OU>`GNVN=D7Tyd;-P+nk&}^@)IK9ewJQ%TS{iLO5uZT$6XRK6MbBO)a3B3LuJe zh*!V{cfYfaPyPo*T)^y8T`KGOMd)KNXoAVcR>IwXtRy6WHc4AyHy0PNP{fllVOb zn9~*s__x}uX%U-?#SGo%X}#a(9iSuBU2M*g;MD-?wA^mCFW1Xro-O9S!9%P(L1YU$(Qkytnk zvV!B?BXB_ilSVfIbhd^s01Hlf7^2#6FiP-n;S%&r&e)P}n4olUEZ&$qi{iM`$h! zmAq?GT}rB(?_BP?E%ju$YPs@vO*@yTR-5*$)$Ex&d8eUe>CnQV#l!E`ry6?a&ZNB+ zYu@IRw|Rc*U1??4t?+H{>GYmMb0^oljVW*A{P~o(D_zwv_oIxAYWIG(;YAg7e@8yK z@_s>gyp@;ETs)JmY+F8X{m8W=$+mqftt%&Pp8oLk+Wvvm{()rg@npplciXzs%^gdT zg~*)-*mLRSg_lfG+Wugdl66pywQb@-WDpnaSoQiXd~lyL+J}+e`CH123;Y<9H^{?%K60UU}-iZ|PGBP0t473S_UO zJ1`33ZSmR2cqBR%@k@?V6`-25!Cc!5X)~G8CIcieVUiZgl7nFbR%*ti?FiBwL4+Qu zzue|@BWPtO@`i;cOq&l!ryvlt(zTqE^yb5Xvj?HjN%qiTAuHPE#-i{*VMnnAYIV?I zCn(T(loh?z#1Yf5jF+(H#vW$eW?r%@MvSn93D#ZyruaboR6-L}afr>%GiVc-7YDf~ zV3zqer~u8YQB}>GQpd z&alW=KF=(P3*w@j?06(q`^d%Et)nULW9iyXfJC|O9)kHB3X->6D!W*gE?1Xpt~Xz6 zPO5uXn3c|(yFc8$wy!_6uRrRL7xJwJ#0$y#Mgc!H)*#pIbV=aDJ_6?{Ay-e!)>3A72=M zx9gU?+T4F{t6Wu1)v5jld1d9h^)gV(q!{=?>ws4voj^HpHdu^{^aeRNn}ApcI1Alu z3FV(1E(dUo0iVu4BMN-LF^Wqi9vz`PE^eC5COoB{9)NIylM0vE!^%HJO7E1%qX*Wb zvP0{e@T>O6$G zITC&1Qb_y%Az3yZEKb0)obSuP9}NH81h2R)&Twg}?YOgwr*SYwGfIh;IkF6ll00AQ4E>0E2+O||r;`GyautWs1JzmL%1V~g|&1-S^Je*%y? zhn0J}WWSbvs9i_jM{tKNT=bt@&jt8Dk5JrpOk&FrN&z6~gN(*u=ZncnO65&qOOIwv ze1tp+0{|_^Nz^ke!!Y+e9)??&5VPg?s0@Bzp^iVIAK#Vh=W5pE`jlLsY}majcfBHP R2r-6HlJ#BRAWAsm{|%#p)tCSP literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/profiler.cpython-312.pyc b/backend/app/core/__pycache__/profiler.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0408e8d4d8bee14740869097201faa6cf887cf1c GIT binary patch literal 1787 zcmZux-D@0G6u-0cb$61@w$0XVX`=BX=$dv5l3Gzw8X~sZhS*lD+l67WbGMmfzIx}z zbmIn!Es{b-L@)&{7GG>hg@P~r69iwnHjsF+eqc-OTS{6e;)Cbz%w&D=!kqKFGv{m0 z{hj+`E@vQEFFg9y`9ntNPq9gtnt*j5z#_6y1=*6FaV1ZxNRq&^oAKm|+{TKlcxpxU zw2Ic=tFG?h3T|V~&3Z<~@VY8p?Y-`Hd$~$ZLK*ZXvhi7DXNj={`NgXAG>lT$-1Ejb z4eE|d=z!hgwfhk@fW;TjRf9REaA6G_vh zC18C3z#<~3B0;+{wnSt*Llj#kYF)FHvr0v4s3na*Hr_bkM~u#R#Ak8h_u%)qPLpc0 zHOqBPpPXV`qlC>+zm~M8ok;@@_VE;vCQT|v}(YylwDHReQp*sFdPhU^r6m@iJH`Ud7mm*t9v(Brw z+El+pwsujT*H|vKU_B|C+LWS>PQ7h?4x-z*Ewe(->rH*Cw_}+i+pvwRU1@J51pl@* z{npV+92YI%}*F4rfl+BETPZg_$3uz&)qcs8~IcvO%#1XB1c z;+ULrB4!5Dr7kME&DDrmlyN0uK}f~va@h}Nft`%j#RO2n5GrPyy0`o$26H3o)sY>qZe{4M$)%D!gCv?nh-S-M?a9P zhX-LZ`f_!YMAJ}U_4f7C_bz^O=aVyczy7{jpK)D?Rh!;bw?K!^E=sH#t0wKJ!f01( z0VUO>VYD1JV6>emv{-F&?uq{&opNd*XcB!3;xF{*GP=!-B$6fAC9+*#};y{ccXMR`&)g(%XrUfe%nHRZ8IwD{5ui_e^eTI5hieCSh+fq1vpmHIW|qv zXVc_{DX28#3fyg)Cub}-{zAoE(yS2T2BOb_xPsO}BKoWl-Kod}h@J*<7OiFSYVr5Y z#WfZ5nuZEPYq~%f8CxD?0nuHl*H|gfb=c^;0vERG$7Uw2Nlxf|ENQy8^t692^Ut8p`4 zqr3$|7DGxD{9>0Z(chTT2^yF{oojd562ihj7c%y&!Lj<}V>|a0# B&{+Tg literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/rabbit_mq.cpython-312.pyc b/backend/app/core/__pycache__/rabbit_mq.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72cc19d3972f19718397d94b9e7bec1854a2721a GIT binary patch literal 2840 zcmai0U2Gf25#GH!lBW|%i83Wys$tWzLy0m(*$z-QPEwSDD95mAStR46gwX4ecd|*{ z4}EvEE!hQRJ7`@XMdO#qKoGPD(udY5AfPXOwO{i>MlC2T?4U&)w0WZ=7jcUg=e{JNE4J^12V9FX+T;!8X{siV<2vGAbY$%Z!Q(7$mDQ z8e3qo%W(w`I8OCx{(|4(d}=`B3%rBdm|I1N5OF2R{kjV_dE<0H^ZHVc_~Y+%9FH zlIX7Skh1F@+zV}TzY>%qGPB6Z1MhJ6ToM%q@kRP36mm)iH^{7@do zThO=iz@A1JBFgUQucJ!)3plpzLB`%U_!$NZZoAIK*C8HX18Dm_U4`!Rod#AcwE`bb zw0~O-TF&ti^7u;FLtS|tZ`-54%X?^>cEIDha+@~VWj(Y_JLGX)Df>S0-)sNATSyLE zV-b3vSz~|h^T91H2acn6eSc)H-~_)iDa4??A?dQJ#GDTl)7+06TQm(V=EYIcu_N3? zTT@9OS1VSOl1;c-upvy|m`(Tyf6=gPUD6c7RSnZ7e9cl!XUf z;0Ig+w$q@zy9s!{n}8kV^U|+ESA3^=MG68De>)I+klnF;uD3u(-2pqiv0hlez*kSl zMF;Hg)lCr$DUyXg3BBjwi_ZBX-%y6$cg}en(FDALuChNySFn?U%*vFLTG^W3Nwyf| z%93Kn%6d%FW6EVkw_~bdRNW*z3%M3*2b}5DXyPxL1}q?flA-G~F?|rNkb=IFYFNte z(H8y9W*i1GAeuE@Y%h2BG3oPi7`n9rgBzMW>fO^6_BDikO(EV8;v2$vlN+yd<1L2u z9ex=8UNbz}2#+?y=?y*Tv>cR$Ec+FYvvYH_) zs+Dw~M=R;%N9fQODDns;9-(7jqC=0+(ZBca-4r74jW-95GzN~;BS+UG>py6| zJl}YEzJ4rQ&*tj+g=W6e$XDvQrTUUu*J|G&q~jUpi7Q$NX*kV*NHeokoMh%0FgeNm zgqlzRVYzKVtftc({Rw?;hU3@fOpM z{l7iaLLk=jUsJKgB4*6tx8giL+!{h$@b=1Q?C>Vnw|epyr*E8Y^p9@zANnizQj100 Ih;u#v2kpF(PXGV_ literal 0 HcmV?d00001 diff --git a/backend/app/core/__pycache__/redis.cpython-312.pyc b/backend/app/core/__pycache__/redis.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f85e844ee6a3b08932f0b15642591ec00ea545ed GIT binary patch literal 1715 zcmZux%}*pn6tC*9>1ml6SVaVOq51G*6ehvMpvD;2Wk1M9Fn|XqlXRNyYG~-`9;Q5^R#I$|Ys=7vC zd#>)-L9L)1vba1BvlsoEYg{ERp?cs^u!xaAD6&t2z;;)BBRM0Q;D*s?KL^t`BB+Sr zR-B28JP}Neh;TZ;CH$9{R1@-IcrM!Xl4BD$xS|_yQmuYnQDG#1s^wADphRaUs&UFj zfy?i~tL-i`BrIeD%uVQ*h=|UJiv-&ksVO;?W@F%;iufuHl)0aM*1U z(1=EfUjY<}A7*{eSSNuN*k$5XgD_XtZ)kwEOCsL*VK+cbYntbFT=EiS@KF&&s4QHb znO)Sb%`Yx>^mpeMmO?qU&%7OGVr^;moy+s@E!{zF2F!z55HqirW{AHYc$Lzx-)|m% z@%6!{H-G!`yOLFP96!*F^+`*IxnfTOK1NVl1`~13l3uBl43Cl$WfU(|YGH3gA)+M+ zYB7cCFFa1a!vA`*4)ZU9*hI$)>h9g|>2LOoH+sfTlw4mkr#5oxUhnz63#0pkW6i;- z#^BW6`JufFBgZ}IZl#6NX@x!myUgWNp{!}HUM89rDwju;ta@gQ?f~pV`(C`5o|5JJrD@O=KYXWtTHgnUB-1Khlwau=UB;(BO Pp>M=Sf8iK`9ewu0S4yiG*OHnpH1*tPS~LSx#HS}AqfgHT!4+L68XYFEsT z(#Xi5ejEy=ke*y{Lqks~KK7FTp%)1S92TdfG^Y3_qZD%No3$p&gPr#^^LuaJo40?a zQYr#^`}tq?a|xk;n8FG4kcR8Pc94b2$Pz5k5y}E^$r0;PSrV8{cI0}b9056EMV)9p zR*r>o%u(v`a$G=I!Z94LiPL~oRxJgqHPMQHCzlf8y;9z>xctcviE@0&r^(C73peTOFknvN)`)5W@l>?I6nVLfR0yH0G>4dn5ZsWNhaW zzvwoiQEsCOmn`WQaa(8$8wqY1^ETQWMcx+MqJN&IxX+T?Xcr1kGu#3Qr?@=kJT}MK zMR#Q`e=Y1FW-{5AKM z>DboG2#eZQuBa_HeX6ZuK!>lks0F9OjS%19&qz-MqUQ(EHB2D^7Ue5U2?o~+kb!kqf%=+j!(_%=4{Ob-mqs0HmkI<3MTAY6{u^a z>JbbJo0N)S*se_tqu6K#Nyz_2GUOGAl7-LTg{6a@DQF_UqyMD;ynKJYpL=INx6sQi z^m7+G*PlgEZsy?h`QwN*AMMJM0~MWqeLt`F^7{P?{rp^~^mk(VAU*vkeeT}%dzS}D zcx&ch=3KY%?tWpRS6Jv5F7{_Gb_esSBr(ysF@*Gxo(f`yVR==V{F);C)X9F~Dhf%A7nUS)gOpD0i+)!|J!_zHOqR5oD# z2e7~e1mRc|1$n3-L4Ay-4$*}}q&-G&JjqPn`LbJ>>*nWrnfXp~5SIvQ?060( Mr+&EdwGu}7A3WV%D*ylh literal 0 HcmV?d00001 diff --git a/backend/app/core/config/__pycache__/base.cpython-312.pyc b/backend/app/core/config/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..34bc50f6c4b8320c58b072196ea7cbf9f8f68341 GIT binary patch literal 1891 zcmbtU&2JM&6rcUJ*A9*Y2}$@`ph7YwHuO@2C{iH71c8n0kXEZmt7UiWY*_DZXD4Z6 zrBYC-pogl-$sAgh_C!T*z4U+RCCHWPj!H$ve@tQkTrgcv(sJ=84M3`4EvBMmP=%?tw9 zu}tjvUS55|4wiBp5wDndfokQFgCR2A98c0LOSPAT4E3#ZzE^YVQ;vm6;_PH~+L)TD zSn>U_lzW@*Dm7XF0{TQi=jo$yx?uCV2qh@n0&Bt+btT$KYzGszJjS$aD^WC|C!=UG zimJK_Q#7VVyR@CL(@?TgO-c@X01nhdd&f=bK0VKtzGEeo8zdRl!(UaDVHu!mu8#C7BCLNQ-wm!3^C1|$ByaM zeb>*^8VXLMP6NvhEhHa!b+WVi_GGa*T`|s2&dr=FmyD~GONjjyC6a*NvQcwfNMr=@ z67noQLLe3rRg4|oMJieV_m~O*e)*NnHB;N%t#cpns~pO%@{8>3D{GbObDRUX|5t)l z-sWxL8?n#BD#%IAOA*oxx+28?L{y8g_5+5B>1wnA2l-wDdRSwPX#O%p$gyGmWs0Vl zr(ewy*{ORzf;j;V(#psn>pgZEuo2)DBEk1DGIJskLX0AWh)fv|JsUAz<^&>GZWy0V z_K^!i1`)0FMVacUQ}a_WypH`~{`c?Jo_zk*AD?{q_=|7nYfG+6pUS#E(R;E91WgOi zU!yLXf1X}5Z_(nYVCv5fxAM2!0pV~EJk5yf^8!;C2AwJ`Efco|tKq;g;)%H0^5gY00`_F-ndgn}ea z$8T^3P0#@Q55pQ2TLQ;%&v>2_y9vOl55ed|F#Z^v`87M-5#?LO8^v4Y8|C%0t>Q+p zRo*Dyz0lq@(H8Svfgj=@^NAZrJ{|dZq$O|2t>um7EeUYR+pl#2&+UJ%j&ka{`vOoE G)$td2Rn(*a literal 0 HcmV?d00001 diff --git a/backend/app/core/config/__pycache__/components.cpython-312.pyc b/backend/app/core/config/__pycache__/components.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8c6fd16d78c2a7e3c1a0d12f1ac72538a03a6521 GIT binary patch literal 10009 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 literal 0 HcmV?d00001 diff --git a/backend/app/core/config/__pycache__/production.cpython-312.pyc b/backend/app/core/config/__pycache__/production.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e57022c6e289ae0e61129f4273404c3d94c1a691 GIT binary patch literal 1531 zcmYLJ&1)M+6yII#N~_hkT*t9ngMulo(pqlPLrMuLvLzd(vg}H>3yWY_?~d#x`<2;M zQt40(O`(^9Z#k7hdn)Ozm;MjE*!UvmP$;b_z9rbkJ@(D&YY;!Z-<$VlXWnn-{gFy3 z9E|(_{b~Lza@^mHj7NH8T)qI~C1-FdXYht#@wT7}c0`Tvj7KcdmQ)FB(U7dD9aCeW z9kpaTuEuReRqTYCu#;-iPN^w7t)}ganz6HL*3PLpo)fqY&WOF>3>n987ArW8b2y39 zID=C-@hUmog_`fhi%S2lvQ1p0s|Tj*6nq>6rgPvIIxZ;$hq$15o@MG9n=C08EU_q1 zdCk&%e@`PcRyPAoG^@zdd~1}u<~kkopknI51<&GnnpxF+T=u+iL@`d2bSj z@}??c=b1@d$Hq=jcy0!V1wSAl2dFzNNrB*}U6WuI;FrRss4~q5g(b_?HH*x^9%!Q4 z*sC?S8k^O|PJeM-^Mmqst+3v;tU@c$^e2Vs{%CT$x%IfZwu2hw%_@z3S#CCJjg6v2 zrAl>mcY}(x#`+eSW6Mg_W^=0vcd1$1sjZdkG+y7@K=tZgwcZz3G|!~Dz1mI{!N<^Q zxm88G&3ZptS}DQl`cgOOEPt{?qvnC*5?mCi3~tmhElhJ`3t>lh4Y)Nl%Ur`ygl#YA zQ3-z)5RE2n*T5Fi!z$Z&Op$Sj-C54a98t1DXpdcyiaX5E6dve{ti#5dZ)H literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/__pycache__/__init__.cpython-312.pyc b/backend/app/core/exceptions/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e755deaba1ca228913725a9f350d6fc60575f0a8 GIT binary patch literal 517 zcmaKpKT88K7{-(P-r|Fj^qTpDbCJ?` z6gEPbQiN{7NV*6hr&;iFe>%J#yk4BY-P{G8V@gGi!(P3E<(b8GPH>n9 zZ7Go?3S@>r#SDI91_;GToFn9A{lZyiX>?4<249da@_QtXEoj4&MIDrrx!1Plsl#fP)3R;S2NCAy3xf|~!ahS2q?4~rR zhe++UXOL)B)DsmwRO+SwLobOOiE5M-q!v}&f&!w)&Kr9iZ)($(_PsZ6=J%V~nfKo8 zZ?RYe$>)uK{>a^x)}PSOk6AIcrwoK{BL`vR7*2p1*pNC%11yMx%*3Vvev>w_5Du|0 z4y$$(wOBK5R(6O+SPO1pQ5I$VH`7kmg}am;q1~(p_b9uC z_OdvRD?3X2SU>Jpc8u;}19(8$t#ptL;UPARhuL1dSNYm#f{ox2Hi}2tKDU5G8r^^YknFiN7CFm#fhQL1yh90h6LYfREs%s=EYe)o z_Hu=MD)JPVFD3(`dGe;6^XyqlgmuQI6fEfuPUe?|nYCRnX^7b6k7q8O{US?>08R!) z_ygH7dFi|ePm>!<#Px(VP26H3?-Ce(Zl-p)A$lvLuGlo^s0FAc%At`<%O75yskWwx zlXG>eW7_t}g&fOyFg0Ow)Czs!qq3~fl zR4%Ja)%SUI%ls=7gS9=omnPR8o2qPp+9uv2k6X0uIsg%rF(E>ncuPE=t%Mr{1`5kJ zDBtZtDQ}}M(PC{cjF68PC~IjoY0ELZkhULG`n5J1@mLz+<56V;dNf|^hCI=_JgwTR z@me?JY1atfdOEdNUn6`x2een?wQk6BP$PUi6WXisS~ui5tPwt*l=f=8)&`OVH+bt* zBq5(^GM{rj!V|KFCR8=@SKvy;^jBd3iuP({Jy$N-7n6OW$9C#e57Awr@l{w38OM7- zK$>$1FdhTZ0Rp0#Lnv~{1l|P#B7s9d^C1urN*uC>4}gH|=WxaFHV_bgqPet3Ol{_EG3r{DeX*kfmW?0k4HSSo+I=-8>in~&pxVmm$`+W!Z%K0{|I*|3K7tS3v>sok?~Sary2 zx1wL8-z-^g?f%vct3!96C|M_8z)o$;Jw?gaML3gj3fW9XglS=ZK9`>tZP@~)Bny87 zZpxm`a=0rv{LP7ww_KD%INS&-Fhz%*E5P+ebF(Rx7_JI15w64ohw{&xK|skb$M8OB ziO$N1)J|xqC>5>p%CB`ou#0wewb!~K-bJfR3peEjuIGl3md@&p4*-=TVnl{w3PQ1l zBc($zWiNRPDxt*KE63>{k8*mcY=h+cA9nbaD5tQ>UPv)9`pS6<@}G{Y+$^W6%3(-f z(O(ZIkA=A9yoZz*&GW>&Adi!9VP!ITn-O`yL3zM@M0Ts3`VsMQu*YD$b%GU~B}z{5 z6S4_u@7|SSD`*(Tvp~Qww=HC}Jwitxp}{BU&?A&uL#a)4Y!kh`iN-e3_$E5Mi6)+) z(=P+25qO5Acp2HTnvB3UlHz4l`?jJe+_y6SFce=4#qU_9(8x;gao50|tM{^ZF5W-? i^TOKU)2oB0*Sg+cMeSRmlo7fY-$GK|pLq^SE&g9sO9DUu literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/handlers/__pycache__/auth.cpython-312.pyc b/backend/app/core/exceptions/handlers/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbcaba6dc9ddea1c44f17e8a1df00322b633bce5 GIT binary patch literal 1491 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 literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/handlers/__pycache__/other.cpython-312.pyc b/backend/app/core/exceptions/handlers/__pycache__/other.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb424b32484c49baf5584359e6001612fd19a1bd GIT binary patch literal 2796 zcmd5;OKclO7@mE{i660(CVi4s=_PHlty0?ZD33x{Z6LLS?N+K~SZ%%2bemnTnO(Oe zawJP|sSp&n)iv@`$w|3Cjc zzW=d5#bW&k+SEV4nL`5z{YfY7CWgfN_dqNo0~L_L7_7zEY=LDc%~_ny7kFDJ2*I4U zL|ZCI0WDa4cBBvqXwmAo2MPlLEm=`JR)__3pB1+gg+xF{tU)_jNCMrDd1C-eMifV` zibm{XzK{xeY{-iPZz$vqguDdshC^N=5 zKcaUWXZ%?1LVi~Lu!Jj~>6FJ=U&`VSs@U~>(e-q%>VnQl$hxFkrlC_km0ANy^zB?G zoyBg&DZ3cv$*k_-8PhgBY7!gRpj%kZoQ6AFFn(gIp+t#bSgMiGmM@X;7#ythMS{I5 zDVNv~89MaA$HBK=gKiml(6`$4UagR~#2U;J=Lv0{8neo@=q28hI$Ts^YHW?G@dkUB zTMhkyL0+Vz0W#1)cllMm#mk{_Vev@64DEdIx^j4O`9|ltTCWeUPPG;XyVq|CeCei6 z%4T_PMI=$MN@CFY1FiShN{)ej$;Pg$&tYE(fb<0m-@w)iPll-eFzr&bqszwyLhtDx zIG@kYYA;MpX&2M8+00vNE_de58C6r$`RV*+pL0Dz=*0;5KUxvGTenPgEW6YiJ-NL3 z9#CgrE}q5iyysMk55B8E`s$m9pMLb$oo|a*tCj^vF3nF~)nTldlK^SSD_#K>E*nL? zQYn@kf{PtjaEon!EIQOlmCAy@zx!%kpV2}+vJdtjgwM@FS4SIhG_>cJ)XDp)ll6BR zak+l(*J$E%?vwL%^#Px3430LUiN^34CE`Pk_~>R#62y%ZitY@HC$R1gc^*P-v%NE2LQCdEeg!_^a z=-NK>T;MY&jvdpc(|I+UJ~N}`)a)fStEt&+CQEkH1s$g$dtlmi53(1g{)lUPm2I#a z1K|YK53%bDusd}>b&6v5>JHfL+6g41$JqvzMqi8XY14NW{mr`OOM zYe;E|dl<118*Xwi{t+2#3NSWBB&OCSO1(J3h+m&;A{g#X2Ez|C>vYzfq7o@!{j9wH XxM&nab4>)py-YCt7~P<=;LQI5k&LL1 literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/handlers/__pycache__/utils.cpython-312.pyc b/backend/app/core/exceptions/handlers/__pycache__/utils.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9890777e18f0818d506644d25cfb357e4d2419fa GIT binary patch literal 2757 zcmb_eOK2NM7@pO>^{_45vHVD!t>e@Yi4{m1O5LUql8{1-0}Z7h)QF8{B+FS#>g=kC zQyC2H!9JAACp(Z5w-lNeB+yHbx%X1p4i_025Wj8LpbYSQI=lwtC z`)0onh5QK0slR^8zM~@aD{pwEwV`Z124w+Z6h~OVK1+xTAVteJDaOS~DJ}{1emO2f zzho(sYFrhN4-sLa&D*RgvHVD=x8gdHu+jsKxL-#)!fjY3D%MB<>m-Q%B!mO-`EG+{ zI0zc(=@#7bsT^-{XNE{C2@x^v<9nnf-1?~$Z=3Q(!?%b0?>Sj3>r5HxVlL%m^Eul{ z=cz#`g?ik~VT({Z=Kq6RG8XkQ@my|-siRqkP}7PEOnsMpSR}T?M6*zcDojqBj%hJ1 zMYB#e1)A(`#nhW7&1G{LCS`N!Jd-iGUd%9+daKE;6bsmn=@ji`Dvt?MCy8yF8A5e9 znaLLUh*(Ua#3|BTO5x@-3Lh8Y+uHQ;@Rxi}BWwtj&;<1TvrI_;>_Ny2HLTdZcVi;- zYA>&$UHkV>xL0NG{2W|`l8AkG@#=2i?id4g2lIB z+mb}(l2lT#)5zzr#I+!}yY!zNHU zdE+X{Idm%K&Kir#lv5Ow3Z^s8lu6=@=P}b$Ru*O@vjrA%A)0Iej`G!+CJQDtCvB#V6B7fOFr|L7 z3+!kI6K#i5o=V!uE54BOEHY_454@s|o3`mVl*ybO2<_%WqLt6M*3{X-4}OEcNgLk) z5+M#SNY3F#9&-1=*_1>3xvKNQI+cV|W&yZ_BzGbh)m#rKhnX8O%30Yxfrh!pon*nr zS?(@2zeGG0y!`nAG%k!JE)e^MlP@GTznl5vi?4qB?33T`eVa%ZEejk--55@rAPd=H zO3aj#xDH*Av;?qcB9*5kv15*%@aQ*D1dg_2g((&Sznccr6L286(H;QGn?bd1)O}>V zt)qNt;qv_Dwa&px=io})(Q13oLTo-(J#qTU*u$~%`wQ3RudN+8wtV2&GpX8Rya*^E zW#-~`8`4^5Z+<>KH@#@Bs>j#+Us)RZX86nDwSke!z{pDfo3rw)Rnhu>QG-?EC^T{v z?a+oAEbGf^&$<@)JTMn1Us^gd8+fioH=(W1>E)rN^Y@Fh`l@zjQ)*e0dMi?IRSR!+ zbd_%|d_4d0O2?6E*P(^ne6HFzu+|r=^u^XAeT$dwUAc2*C32!V{MwUi53iLoe8yT= zbh#_~tP||j{oArGE441vKQuF11sl|5HBycA&j#INx+_xm;=xtv)pdTTQIU+Lj_1@x3?}&$Dcny4e*6+hb`@JIG zdMw`cT}4sp_9++PmxyCDFlzw1O$<-)u2-BAy8!PxsndZAAb81L=|V25P|i;D2(JuY zaf~$~85LYy*gS9sJq|T%%K(l%n&3y$lhAz*KKmk6GiW=Af*s}87tYR~T{^KEIJqsN zNblm%z2Q52|CEz8Z<`Tp9%e<$DK^f0)H4V>_UnT zagBTQ0J&Dwoz~u(%;Ta(-llKB5RZoaHB>cG5QG;#pCD~%NND{T9sU)Ce?)y7N@OOp zrbH@AWKmvG`e($AK>H6Jmn(tM8LifadJa{?y)QyB;nixWvnGPv(A#S=$eM!M57bny zX{aS!)4AqH!Pau;+(<3JTM*vX8edy$L5hE?m21}pT~HP?H3Z`R;C=h)`TMsX#I||U G_2O>>wp6bEE@hr{zx}__gqXoZc6&26Zl5bcB1YO|jLukzn3f1CR zT2x_D-fOiEnx_tlVlNDYVC;rL$Lls33z zWF@^!q*A@8EXh)OVJv%8Isc2+7s*r~$!hvlC$nj&ZWGe(ED^%1C#!1jAqH!|lsTM#ARi9ykw literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/types/__pycache__/base.cpython-312.pyc b/backend/app/core/exceptions/types/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..64459ceaa51121e1e829e7ed26bd513eb9c727cb GIT binary patch literal 1339 zcmZ8gU1%It6h8OP&(2P>X|wIdMPeE1L!C8$VT)VB6u^wGBT(keQ z{Zf!|hp&b_&7-92^XG>xM$ke+$_p3HPfQG0E9dFkbYjIg|~@T2_-05ciwSewuxtg=z_OlESULz%UqdzDRC zC7$&bxVDhM`)}J0lEN%e{112-2U+HP80A-;fU!tI4q~Sh@tBqMFrEid%$dW5!14r7 z6Ol*VEA#5gR&gZDgS?mdVZwM(S>ah0T;YYGq6<^-yeGP$J}yuS*>wqO*F*Lhxd8(* z03Y>|1{5tau{5-BZYAaBfwrX&;Q9>6Ty2$Pl)!*~OKy)N39NxjTP_vyvBE5Ca1CV& zd5ORVLSbaRl#9ah{iqw|zV8}^p7FSIIIimZs!zV3$qQvK*Z1B*?c!qlBG0bnN!tGF z&icJuU;Takqkq2muHEUyG4>_AcD56sOrx`c2VvfR507}4wQ)h(VIp{YvO!r}tq?|J zd_G+hQ+RuS_K}sDKY&?7U$%g3e;r0sFn4-W|JiEpJa%$Z|72>X@i+?mH>A6ckoE}=uU#!=6F$=;;d(n`iP~&(O6&wba)MrPu;Db8qPeqy|B%` zj=zkzFAtBN8(QbYF${5Of9^up>2>*P%ENrJo=F(?gy5{@R^?c`!3y{N!uI`@g!N*n zPx<}_y&xWY5E4s5Jl3LaUTBCzbzamZ-r9KVA&+BR4kD%{LRrv@svz}mDP~E>60-db zxKF8Jji#ab%+8#1zgjikAb&KPBSR`96Y7moMX44XJO1hNR)3^IMmE^fdq_fqlzFP_*7IfUFNh~FU82wCkc~7# Y2x2Aro&{v_fjL9Y4^KR`2PjqcLnI(iumAu6 literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/types/__pycache__/config.cpython-312.pyc b/backend/app/core/exceptions/types/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..becdcebdbbd6d9242e7557458b13d3fac73c3786 GIT binary patch literal 1086 zcmZ8g&rcIU6rR~1ZCPkk0tBNWd)*)nF=CX6kw}9iq?$rJ)FzW1&~PA; zL^zOmF&qrhc*5w-i~oZcIY~CeixR^Pf{h$~v;Cnshwr_ax9`pOzIppOoi-4xtEb=G z4h5m_a?@IR(>b{g&K9DmfGDPlhs#Pq!O~YfwX791j1)A9sJ4cv&b0SxBd?$@C#+=c znsGPqOYVG?+o2oyCR=h?CHBJQ3KM22;AXhMOuJI?Tqph}V;ss77LF1kw8N?ZFMV%z z_CdCl>ol%tpw`@+Tmk1fFlDHKfvrMuL8S`Qs7kexwvI}e>T6vEeH|^~LShD4iKw$d zF3WkqYvYCqt9?mKZNc`b=lb)ebEb=@w&&7jLo!!n;@l`%Wz(prvS%Bx)TpzeEpYJ26_EU}7;0D#ah4Ru8w| z9&A4Q`TAqARP{XI<17x9Y_Jt~h%?&>i*s|b| zU+ekjK{GA>=O7{m0Zv!Zek#3@+)efG_YZvSAKUF8+v^+LncTVgrDtTX+d4|3j*E4q zbr>h`IkJ*c1m7+*LL!5ZazLw|^t%XoQnkHiBuNMj95_}*$YUN+y4k#?tz;+$0FG3QG4R1!J%#Y419an8 O!obFhp*jK+=l=o42PX9Z literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/types/__pycache__/database.cpython-312.pyc b/backend/app/core/exceptions/types/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d0f7e97d8a80fd68c4b146cb3cbe927a02d6e317 GIT binary patch literal 2312 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 RH~+~T*Yd0NO#+kR{|~-ZXEFc) literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/types/__pycache__/validation.cpython-312.pyc b/backend/app/core/exceptions/types/__pycache__/validation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3bd97056d252fd5e60e1a1d9a819e03c07a9d4c8 GIT binary patch literal 3306 zcmZ{nO>7&-6@X{?Z(5Y~=a{B#Wo#>wsY)wbl4Bt?;xd-vD5zD%k_uEHuwCsArLC5` z?Cdf&7ZzZkM(Z4c+J~S;fucowY0=z@0zLIwAU);O#kL6oBtejy3`a-~J@maHcSxx& z53}>WnfK<+n>RoFN1>1>@cZCDA6vgF5b`Ms@h{z}JbM``zaS>rB&K2}Y^9#qOek1S z+R1uqGo_FOxkb#>Lt>^`>QBkG-e$U$DQAN#`S0ttW$M1=I+WcrSVPqOR)cx8=5p%q zFse5iwq=MuYW_>qFDp^T^L4-JL9Ot{`ugfptZTIGx64#Vcs2!6q?xLv9+8@2rXTigrXP`e%4TMblrzzZu4hY}yF9p=r|^>l3HNq%$FwbHn>Ibh zspT}9K84xn^zEK$p|+=&;BCG6yyL-v| z$~}nh?j=Gv#MDggC7izlNlB6h@;4wENwPrx0wgENF(B^)$xCt^$nSyl36cTJ1sJb? zFX{Xe=n2_x0Q&tL$e<)cKz<5jSdtMSdq7S~@&XVa$SFyRKz4zg76ivD!FXq2ydMHR zE9p6)qd+%+z9=aLdLHPTKrcvo5$G7u8$e%@bR6ge(0QPjBrOA-1X>3AElH<bXU+?jGOq%8c`xb9Wqc=fpS%kz>Bk93ew2}ovT{FhKl%Q( zct)OG9@bo50lHaN}t7W7o3LOh6YA(&W2b8HH%F=Lmu+$N_T=RnKnEvj#W zK{b-jHFVFr>vA)gTJ6;6|2VgB)HyX8vK+(ZoEg3*EetKkj7oM=T7}?BooFELpgFFu zdCU*a%8r+IbZ>{5WeOV5N})9>c(`V`8S`)Qu*%==id0 zG#s1o#K3jkH+F&-;-Niix?)Jd;ee?({Fa7?P%!?Sz>3oeSEDDceers5*Jii4!x`f=VH5E6Fq8CKh9Z1x#l(s?&7;l@ zcAMg3;dGD1BaTj1Xt#gQdGkDX&x&co)z;PMar>-@1Kj7`)*km~4!e(*=;6LcVth&_ zf{ccSP6emr^2!D(8s>UtDcp|}WQ+NC8FT0radut3+;eJl&l}D>IFt+)48-;&nYzDw$yi(8|d ziqc*a!X4!_&2lVX(>T5>qI39M(&lGpwT+e4JKwvxw6<1x?Ym3b(#rbs`VY$KDCIG` z#pY&dH*&BGT8zJwkeXi!^xrMk$xUEgg~Kl|&0FMs{J&)@yY7r*&) zwbrz4aL?GCuEABIH>_#ciH2X@0>m7%3Xi%f9LB6FUwvLxJo~EMr<2-f@ry7hMJv9V7^Hl8NQVDTVBhQQJ)YEGjjm{~^Q z8Vcvb(j*GsMqxB8&7klq3KL=JG71$G#=_E76c$mK2}=toETb?PmgZ2njRJT+g~IDk z(*KgC7aylT%N5?qewZu%yEyvK;`I-U*Ta!B;n1o5EID@ifTWJ)pFwm;Ig1&9Ij?C^ zUeoHX*@QDri~2O}$4%Ytv}85SbPd2s&*viQQK7Tu2D5EVD=T~)qYc3(-{BKba(vT> z0{

V^DnakUUK!(z*RyMM>v>JGW2p`-l0j#rLTwIXq$8YAfXo$Cr7O?c_*1Lq{Pu zRLl*vn;WXg4Rs6N2^=3)V%R^Dwk>of%3W{I>{UJsB6{gjDE5L5pyGoO=% RuQIPIO8#;EfZ$hH_%9Q-B|HEC literal 0 HcmV?d00001 diff --git a/backend/app/core/exceptions/types/user.py b/backend/app/core/exceptions/types/user.py index f302fa6f..b6e189a9 100644 --- a/backend/app/core/exceptions/types/user.py +++ b/backend/app/core/exceptions/types/user.py @@ -36,6 +36,14 @@ def __init__(self, error_type: str) -> None: case "invalid_username": message = "Invalid username." error = "Username is invalid or empty." + case "role_has_users": + status_code = status.HTTP_400_BAD_REQUEST + message = "Cannot delete role." + error = "Role has users assigned. Please reassign users first." + case "insufficient_permissions": + status_code = status.HTTP_403_FORBIDDEN + message = "Access denied." + error = "Insufficient permissions to perform this action." super().__init__( status_code=status_code, diff --git a/backend/app/core/exceptions/types/validation.py b/backend/app/core/exceptions/types/validation.py index 1afd39e2..73c96690 100644 --- a/backend/app/core/exceptions/types/validation.py +++ b/backend/app/core/exceptions/types/validation.py @@ -48,6 +48,30 @@ def __init__(self, error_type: str) -> None: case "invalid_pagination": message = "Pagination parameters must be greater than 0." error = "Invalid pagination parameters" + case "role_not_found": + message = "Role not found" + error = "The specified role does not exist" + case "role_exists": + message = "Role already exists" + error = "A role with this name already exists" + case "permission_not_found": + message = "Permission not found" + error = "The specified permission does not exist" + case "permission_exists": + message = "Permission already exists" + error = "A permission with this name already exists" + case "role_or_permission_not_found": + message = "Role or permission not found" + error = "The specified role or permission does not exist" + case "invalid_permission_name": + message = "Invalid permission name" + error = "Permission name must be between 1 and 50 characters" + case "invalid_resource": + message = "Invalid resource" + error = "Resource must be between 1 and 30 characters" + case "invalid_action": + message = "Invalid action" + error = "Action must be between 1 and 20 characters" super().__init__( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, diff --git a/backend/app/main.py b/backend/app/main.py index 98f06a66..2930282a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -4,9 +4,9 @@ import logging.config import os +import platform from typing import Any, cast -import gunicorn.app.base import sentry_sdk from sentry_sdk.integrations.fastapi import FastApiIntegration from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration @@ -88,58 +88,72 @@ async def health_check() -> dict[str, str]: app.add_middleware(RequestIDMiddleware) -class StandaloneApplication(gunicorn.app.base.BaseApplication): # type: ignore - """ - Standalone application for running the FastAPI app. - """ - - def __init__(self, app: Any, options: dict[str, Any] | None = None) -> None: - """ - Initialize the standalone application. - """ - self.options = options or {} - self.application = app - super().__init__() +if platform.system() != "Windows": + import gunicorn.app.base - def load_config(self) -> None: + class StandaloneApplication(gunicorn.app.base.BaseApplication): # type: ignore """ - Load configuration from options. + Standalone application for running the FastAPI app. """ - config = { - key: value - for key, value in self.options.items() - if key in self.cfg.settings and value is not None - } - for key, value in config.items(): - self.cfg.set(key.lower(), value) - def load(self) -> Any: - """ - Load the application. - """ - return self.application + def __init__(self, app: Any, options: dict[str, Any] | None = None) -> None: + """ + Initialize the standalone application. + """ + self.options = options or {} + self.application = app + super().__init__() + + def load_config(self) -> None: + """ + Load configuration from options. + """ + config = { + key: value + for key, value in self.options.items() + if key in self.cfg.settings and value is not None + } + for key, value in config.items(): + self.cfg.set(key.lower(), value) + + def load(self) -> Any: + """ + Load the application. + """ + return self.application if __name__ == "__main__": - logger.info(f"Starting server on {settings.SERVER_HOST}:{settings.SERVER_PORT}") - - options = { - "bind": f"{settings.SERVER_HOST}:{settings.SERVER_PORT}", - "workers": settings.SERVER_WORKERS, - "worker_class": "uvicorn.workers.UvicornWorker", - "timeout": 90, - "keepalive": 10, - "worker_connections": 1000, - "graceful_timeout": 60, - "limit_request_line": 8190, - "limit_request_fields": 100, - "limit_request_field_size": 8190, - "loglevel": f"{settings.LOG_LEVEL.lower()}", - "preload_app": True, - "reuse_port": True, - "capture_output": True, - "errorlog": "-", - "accesslog": "-", - } - - StandaloneApplication(app, options).run() + if platform.system() == "Windows": + import uvicorn + + logger.info(f"Starting server on {settings.SERVER_HOST}:{settings.SERVER_PORT}") + uvicorn.run( + "app.main:app", + host=settings.SERVER_HOST, + port=settings.SERVER_PORT, + reload=True, + ) + else: + logger.info(f"Starting server on {settings.SERVER_HOST}:{settings.SERVER_PORT}") + + options = { + "bind": f"{settings.SERVER_HOST}:{settings.SERVER_PORT}", + "workers": settings.SERVER_WORKERS, + "worker_class": "uvicorn.workers.UvicornWorker", + "timeout": 90, + "keepalive": 10, + "worker_connections": 1000, + "graceful_timeout": 60, + "limit_request_line": 8190, + "limit_request_fields": 100, + "limit_request_field_size": 8190, + "loglevel": f"{settings.LOG_LEVEL.lower()}", + "preload_app": True, + "reuse_port": True, + "capture_output": True, + "errorlog": "-", + "accesslog": "-", + } + + StandaloneApplication(app, options).run() diff --git a/backend/app/models/__pycache__/__init__.cpython-312.pyc b/backend/app/models/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4189d0d4f94ae2d0cde2d0bc3d2e1f33b0c38d1c GIT binary patch literal 178 zcmX@j%ge<81V%sZWrFC(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd>IWoTtYTbJi?d7e z3u2z^Xm~ky(bL}M=kqtmq?P966qh6>XX~aV0_lQG-J;aQ

qpipXFN=#xwK}>Fb vN@`AVOniK1US>&ryk0@&Ee;!?U};XOT@fqLa7G|51~EP|Gcqz3F#}luYQi#* literal 0 HcmV?d00001 diff --git a/backend/app/models/__pycache__/base.cpython-312.pyc b/backend/app/models/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc04e37cd272ff35fea509f42227f812552f21fc GIT binary patch literal 260 zcmX@j%ge<81V%sZWqJYW#~=<2FhLog<$#Ro3@HpLj5!Rsj8Tk?3``8EjH{v2QB0Lg zn#?ajJWa-10x7AF%mLhhbAON_fN%#N& literal 0 HcmV?d00001 diff --git a/backend/app/models/domain/__pycache__/permission.cpython-312.pyc b/backend/app/models/domain/__pycache__/permission.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f9f3becc0da187384d0efa0985e8b0213d3da1a GIT binary patch literal 3138 zcmZ`*O>7&-6`m!R%jN%{L`kk&vzGirDIwB2NfF0TYfF@@D54ZmLX)k4#hNox*4`!Q zS+WR)9vIX`WfVo3lVucv)1GWdhXlFk*rS17WJo}%4bsMFgFrVMQcgXzZ-&bsxy=Im z=Dj!Xn|U*D-kbSrDC9@*z4`CI>+i}4{gVmpFX%8w7X^gwA&jtKAceIqLoi*6OJKa) zaGRncnjXbtN{VE96|X5PvguQNyk9i@WjIbF|A_5{VD!~|%5jA5< zOh7I)jj;C)!ZL{ybn|@Mm2SHwws@t_g@}xOByw}K+uP=#PsDyALF>VRE%2XxIJg~Z z{B)O+?@H9ap?OtV|CZzignMp1!*rBEO7P9KZ z_3Xs#Ol~?Y+0sPGsG3FFKUJbcUn|a%n(dt_R>&Hmwv?|>y|`w3mb4oNv7^gH{dSd1 zl!{h`YI?DfcH7dNRxT54hfLn8h1PIEN(`-{gXQ~r8RiwQX%=DaV#y$dmcuRT>;pQw z4)Q%jkRo8DxUitOu}cxLTk&8~k+4VcVo8y)SMgz4@nfG7z2|ld=ysXAUNAbaG{Ohf`f>3=WgL*Jqicit8&KZRXHO?NfDPz5Qxj< z15&7hH<6CU)QZ{~$))4Atg38@s%raH)hywv!T6x6-mYqf)8kcDTq=Otx17%|s*BmH zlXif!6TmNJ9btK4vMWqx-_AmhZ*l(Ztb^ce8^MJ(f(s5JvXEVzo5|;A=5sAvtViLX zdsWUDy((vBR%s(PatC7fr$1X;tnDPMK_LJ+kKriC%~!?CzvmeKmE)WCrN14g$M44O25Mo z0-vk}(?hm@p;MODC;Y5nbrmUo1bOkoWK}OhVNPj<0U!j}8z4-kR9s?h{^C<1ktp~64 zY@jWS(=o7S2iqdXs>KFR(Q`mqk))(dRdqelMP%B_DKN46fz(km_We!#K)4p2itTFv9mfq^cDb(pIUb0oK9NnEr?n_SUl9aEI6tRn>~cQl)io z!_py>5D&cVhMDP4nD_=GZ!*GMup=+j3_8xVqM-v|l3B5xqr;5w`o;E|X4-@?))zp2 z)2WyJXX|rMlf$1(@AN%h+D|UluWd*h*4@CKxOgyl?m!N2(BJu+u_P?L8IB+OJVR1_ za^rj>ad+$pc|6kdAiD7CXHz?uKA(L&vVVTQKHErcx;OEC|C8A8o;ZJSI{oPUr}3@P zo$BMs{nPK%uWzh2@^^3TiSHboy6{MPNVohu%K$w!U!U5TYFxTI3#g}OGfg?%jQ_AX zFmmh_hoyRUq*$J`z)@?&G3hxuMMnzCi_MB_QhwneUfD!dE%W&5+Q=%r1Tq~NbP{1{n zX^8EKJ=)ggm0=;{iAdjK79x8S@$1W$O;XX|iKEe3HY<Zyb{{xLbLE~Se)Kho#OL6R2>K86H@|zF? literal 0 HcmV?d00001 diff --git a/backend/app/models/domain/__pycache__/role.cpython-312.pyc b/backend/app/models/domain/__pycache__/role.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..db9dd0451556f7b3135d3bb218ab0403e6959d86 GIT binary patch literal 3106 zcmZ`*Uu+yl8K2$T-P`-`^Z9R^(oNzx9HEy&f}I3oNOP{SlDn4Jd0^#gx!lg#TkrkJ z>|NVCTIo=Oa70lYA+Re{HSmw{AT9c@88UyB9S10cH!TDwLVY?`8x{R4P@Rn{|Mb8F^MVIM8{sVg`%j70`flF zSCn+ADC;tpC0i-_b$?OS)nY&o@G;pA7DIZ7^NJlVM)Zh4sK~;w-VbtAkMcRyjukuf z4uObdoS1>D#0=6nB`*)OG}+ZHOP-)7I5q^>FbysHTBBWcAsKm$LN+P5k*OcPIA)yt z{@A(m6WQ^k==vv0RXRhNt4vmzRhmiqTqTpMRH*4jio7@SWxG}^xnV}_T-7R^ zTS?F5914Amm2GNNC@Wfy1Jf`Rd~vrVU2-VPd+K;pXbGDdFpGo|T`-9*nu6{#MO`v| zx@=0iV#>O|t|a}gdJ&EoE7gjPr-B+ZWVJ$>X*Jkc!}A?J?#gG=nTf3H&z#Mk9Ur^s zN|z>q%Hx;DCa1DV)m06nlq*uhaD#?XESojZ+;GE&aix~CTQh#cFw1$vU@=gRbs&i& z=>&3{Yyu^p{QyY%%ygPMbJcQX`X6_jkKg&-$G>>spj%?&lGadSFC3k z&E>1pmtlyO%xN&{G#pKBXWA?mb5>~@e;`$`FER1{@(LnV=}=Gc0) zw>iEZiZ|2i;lxkISl>2&k8#_`q4;*#XG78AVhJ8E+#yX{RaPNwhUjaBSx;Y!_n9ZW<|wX3teg?`rYAz;~N{phGw9 zDM)5N$i2YFy}tnT6EMuw}htCp=&p3x#LRbA0CA9sOv&qJQ6C5V*VWt+N6tz=!PQCGG~CVkD7FcKgC2VhBo z9fU6HhD_?@nN`93V__I_B|HyuEsAkvRLYeaWG8i88FRv6AzUrG0f*+xB@?t^U&U3S zU3$9;qtzNK<+sxj00hehew&AYK&ca6Coco{zwI|r`rh$PlB6%&P)4|cTw(OKz_38w zhT`B*yWrr)8bi-u<}q4fhS7*1I<+l+116nrAWgC!?_HAK4ZIy#`0>qyE3ttUb%2NX z9{q4juHO^DERy}Tn*^LCUvp{z10omK(J%(&&3fNL?jPiKH z5_fKv=I1o4pjBrnuW;II&e3w3Q=xgQVCA(PwNmXxdn|=|oq%r>v*B05c_MIoy_6v2 z4dHcRKd)XB=O9XcEi4KGd8=C@b@F@Ps{%voaBD))im7K^3D0IjFzVnTn%4Au92l9Q zCHh*0jW!0kbwTA_7ei{qF6VQ$Gn#7SzKwfeq5J$_CCK-TzD_NaB}hVjng+{ ze@+|+`3ncl{AT|F0Rq~WE7jRCkdq?A*zwdr3u`~}w21CKwUO9Im0`d;2Y|q#TPo>D ztaWDYrav0FaBt)Suxqi*ikkUy@U`7N1xx>5@TJLhAurC5y!a9nbZ+W>0^o-DR)++b zH-t%`0_R?Zd~cj?dvlytwVZA5^0bD9!Qn-yI%_#RE_S>R7D2=C6x_DX8T?UhomL)p z&Cu#DpYV`&8vWcx`%QfZH^9=-BybBIJ@(Py(7nN-wb=0L;Lwt}AT0)d8CV${T8#~_ zsKZ~}iw$#UT~8d5lKl)qjW8^C3^Sbd0C6LRQN^O?-L?$w0>aR)>}e!;dAU9dDrL+t za-~wa>ODE2T*x>-z~>=n*&vE4k|d5vwgsd3g!7E>P2|4CXD*k^HpBm5-$l}kgkPOl znAnrs1PhAuPar>SU%K4~nwdvEhi{JG?fP))e$U0`bMwl)b1k$cU3}1Y67OwSK4B(5FXB(ki08YV|i{^9w%C*D8*;lTaD7nMzA8Nct?IYdbK^7GzF+;4k* z>8c~%n{Je40ga{DISXXNCkVn{Na!KaJ|TmDBcrQi^kbquB&QzvIvz^H52a&UiYj!? bI}7Q>i5nAJ1ca@gqr$2Alb;djxUK#R4X*aT literal 0 HcmV?d00001 diff --git a/backend/app/models/domain/__pycache__/session.cpython-312.pyc b/backend/app/models/domain/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..847c431b45a29a1ba8c2bf90b4750d6befa678cb GIT binary patch literal 1554 zcmah}O=ufO6rNqJc2_H{B-@gcG>MZqZ5>gfKq(ERP7kpY*EKeU*nuvh!`d^lH{QR@ zuE5Gk&=#CSseE!SrJ<*k_z>tN$KFe!!9K`S(h}Mb(o>5a=&^6KvZF&w2lVE>x8Hkj z-h5B^;taMM=W1wszf{5fIOYE{Md9o!-LQlCVPq7p)ZKXZc zQa#Pmx_-sgy^NLd49n=+X*cWTtek|X%v6X|UDL~3c?rpA3K8uyM0A>a0A~=(Dm0}^ z=7mt_DiSy6KmO<{p7`ME#LV>ER7K&+!ou_Td)_F?ip%n&IMi4?a64y%=@ zK?=QUg96nYs+72PokkJR!u2RoB3__tG4%=?|^7%!3X%&Y-*noMW5m#LMc9Scj#$2V$b&P%6 zqZo4oV=o{LSLiv6Z#8VU_r=6G7ANbO`JRbwwj5#1#6ONHqN*ZV=4m0{VMD^L3wHry z+xLSwnP-R)Bg@J{XhIwjpi~qyc2t<+&RIr?lOVoDdmz!vc@UGAYLhftjf1fE`=i#2 zufBQq#i!4|{;pPstBYdj^L*WgJ#@}9YA?k#(K;|wgTGV_dZTVsBY|f-eyvAdHEc2y z5_g4Aq8~wgjyk20)@)~Z?0)H|awV=&yB3M?1yRrmrZB4D;XisiVZ%jWnM_$42tB=ax zlbtg&t=aawch7FTx2??Vj=X;F#zVR@GT!>It+u0$?6xw#d+LpQ^@oj}QU%X{UYF1=IlKV3T1{@Xx(JUG6K^sf?(-&hT@|C&oyz2@ z#{iZbsv-jP@nq`WXBWT}Pg5lPf2Jf!`Wa=PpwrLM#cg!)S9IYSI{h?Ne4@Oyua+fs LoxDV_byNKV{C$=b literal 0 HcmV?d00001 diff --git a/backend/app/models/domain/__pycache__/user.cpython-312.pyc b/backend/app/models/domain/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f606778eaf3f4975a5ec8febd80ea462a4e6cfd8 GIT binary patch literal 4620 zcmd5=Uu+b|8K1q|JFm~aJKJ}*F$QPB82dswAVn!oh?8Iwa1bs9gPN_QWw{ySHG6-U zy(8R-swai2cG4>5p@pi7Li=P3su1OcN|l;Nq`ug-E6lcOg4!tM&FK*-RbTpjvv=n+ z*zZ~CzM1{z_sx9s{hQhQb1)bn@J#&gALiKxLjH*_mB&}^l-_~PbwUZ1ETUoSv81F& z^GMkDTHd6r$w{B)OZqi`vQDc@Dw-n3WUD?I&;rQ@ts&W{H42{33MNBZNc8bF|Xr4Ze%iy zI>Dr9^@Iy<F0^K4jP|=5gecFsY*;J)FuVVWjR9vuyw-T;*KqFTa#d zdeCZ&Xc8rw=QLPP^HPr{Q!kZGN%MIK^TV%>%C~*DBv*>cOqpz{ozCsw_E#+sU=4r? zJ&oxA7Hgz+2cd;|gYXMcr8;vxm0_j8E!maS!Z1%$S*Br)Oyh9jC?zM z3wDl=)`EE2RH!4Qyi#*TibaGzE!BNR0oz(eMXIO}t*@D{t!Djb1q1tRr>)iXYw?=^ z-%*~kt&HDH+o^|cy4_J-xsx@}_A+X771c?%+}>K<$rhMtEA2#78QFzM)Yf5 zUX^d*mf_K!q1#~B$d~E%tForjZno96b(y;ZxZ9rM_5gP~Aj(*nbtiDTfzt&2Ug+=0 zH^urO$Zf_8{Q(so?5fE$WtKXg=Bkm)POwzgOo#whjT9BEG&c)^BV-(34y0@+%r0kH zit6mLX=foNMlqxfThHZ83h~~M;bzj{dHn)pO&w+9M$XDQ9+N(b!Y*Hf1X5Bt%QDVE zR`_!%^J0!UJ~KtxWkuZAXb4-u9GWd651yp zN^;U=iT6qyj8B#`p;j(i&OkyYE489#_(C5cr^uW%5|eo^@EsX##4+C;->XB%hezh1 zB-zXww^3X5>mL9edSPsc*%z|u%-Fx~79YL;yDxt6_Lm=gJcc&5vtX0m;|8=Db2n#T z-7z#1czz7(-k1m;dyJ;RN2xKiPdt-%6!Zg4nyEtYG_oqMdtfyi^(vCj!;$y$)A^h7 ze0axS!o#!C4?E^MKJ1$7`s0B=4g6tXAvQd(43_{(oN$Kmq$z5paCxHEENEn5XbUrat!iF&Up zSEM0QDUvl_kPE|AgKpr3R9;QHo+-P7(T&W!CWmn&xZlz!kY7s2h$I5+&1VStu{7;j zFQX@M<4-)RLGvr=x+IaGZ<9%${NDSP#J>a3IgfCvaMqmH@x=E!Qbst`#>rH*D4glk zA3V=e>~e+=6rQcR)JWxP0wI3TN+%4<9*9@5ufY_hZXn!WfZNJ3<#$N!N^B2)|wE@M^ABA13&VmDV zHLvY;V(A3V^D>fGfIJZ-mce-JfM9cch=I9mYul*{`o)}Kl}GAyou(7O z4(fUquZCrQsQjuM{Jagz;?0L+wSp@dTsPrL3UohjM|M53!#Y%@R66T^iGf4_hZH2Z zHOFgbIq$+(6$u_Td>ar)J_jFv9IeLTvxD$%WO~h1mc#e0d;Ibk@?;CHy^fOMX-JxE z-aVc1z~zHTL@*!2-UyI6H>TZFxf!`g@K$EeLU8<)eC6@@6ex<>;p6rs(y+wZP*#d8@y8Xv#m8#S7Ge(orI^&;M0`Y<>2D5}M-g z29}%KAWD`)EsyJcfIT`jK67gNLW%eQ)<`z@t^?cbFCLzHex`f+#R{zFxsP_S^wR)?)`A7^GtDQ zYUfPbbbL`h^Pq44oj2}XTIxF^R3E(&yQVLMdSK*pUvR}E`CgZ9?D!ACw&W%Lkjq-} z*ZcZs6DtIoJG6w&N}B?hJNxf?Kk8p0*#9J2675P str: + return f"" + + def __str__(self) -> str: + return self.name diff --git a/backend/app/models/domain/role.py b/backend/app/models/domain/role.py index a3d63cc5..f4fe239d 100644 --- a/backend/app/models/domain/role.py +++ b/backend/app/models/domain/role.py @@ -5,8 +5,10 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from app.models.base import Base +from app.models.domain.permission import role_permission if TYPE_CHECKING: + from app.models.domain.permission import Permission from app.models.domain.user import User @@ -38,9 +40,26 @@ class Role(Base): description: Mapped[str | None] = mapped_column(String(255), nullable=True) users: Mapped[list["User"]] = relationship("User", back_populates="role") + permissions: Mapped[list["Permission"]] = relationship( + "Permission", + secondary=role_permission, + back_populates="roles", + ) def __repr__(self) -> str: return f"" def __str__(self) -> str: return self.name + + def has_permission(self, permission_name: str) -> bool: + """ + Check if the role has a specific permission. + """ + return any(perm.name == permission_name for perm in self.permissions) + + def get_permission_names(self) -> list[str]: + """ + Get all permission names for this role. + """ + return [perm.name for perm in self.permissions] diff --git a/backend/app/models/domain/user.py b/backend/app/models/domain/user.py index 517504bf..8344e4ec 100644 --- a/backend/app/models/domain/user.py +++ b/backend/app/models/domain/user.py @@ -48,5 +48,44 @@ class User(Base): def is_admin(self) -> bool: return self.role is not None and self.role.name == RoleEnum.ADMIN + def has_permission(self, permission_name: str) -> bool: + """ + Check if the user has a specific permission. + Admin users have all permissions. + """ + if self.is_admin: + return True + if self.role is None: + return False + return self.role.has_permission(permission_name) + + def has_any_permission(self, permission_names: list[str]) -> bool: + """ + Check if the user has any of the specified permissions. + """ + if self.is_admin: + return True + if self.role is None: + return False + return any(self.role.has_permission(p) for p in permission_names) + + def has_all_permissions(self, permission_names: list[str]) -> bool: + """ + Check if the user has all of the specified permissions. + """ + if self.is_admin: + return True + if self.role is None: + return False + return all(self.role.has_permission(p) for p in permission_names) + + def get_permissions(self) -> list[str]: + """ + Get all permission names for this user. + """ + if self.role is None: + return [] + return self.role.get_permission_names() + def __repr__(self) -> str: return f"" diff --git a/backend/app/models/schemas/__pycache__/email.cpython-312.pyc b/backend/app/models/schemas/__pycache__/email.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38074a335711c440da044089f052d7ec7701bd41 GIT binary patch literal 518 zcmX|7JxeSx6up_5UB|`s1N{-MsD+&(uXrdTsExd47zo)(e2!*5oMemb6txuWtVFOA zOB?^g`va0f3l-T;SQjk4nb8->x##4boP7LiHXDFr;qx&*!uo8*&!~Nq{tS`};J}H5 zgajmU0*8PDYrvfo;I1IIZsiT!uGgydtY|rA_%~BxGvh)Ue>GvT+}1@@87utW1^rpn z7a$-Y90KPL2v>NVEaR%EA#?sS*D`yC87UJtg;MOVFpkd5km|JeM5K+^&UkmLRX0AR zDN6*Ura@_v@s6~7gwlf!lU1kiFwl%q%F;B`OvhQO(Bmpy+&~|DFxW;|SqfK#+Sgeg zzT6-7F0WtDj^D2C!kvzkO0#HxZiivbfSga6>T%`m-Ikn2=8bOwYVX`w9hube_cEPhcP$7+A z7aLLRBmcsBG(Cb0ZV6glT^;ao8Q~__U-&;=F{S01E{g__Z!=Q0Q}J8 zbl4}w?5ZmE0RaR?Acj7S4c~yk0QUefo&jP~cwkmu-yB(#JfPP~bxXeB-_frY3>vF%HeMV2j$+A=akymb$HUn>dH=fmP-BZt19L z$EfZ#WUG&NS%PzxCVk9tEV!YhCzpFgyhC|A9ke+OQ%*!XPupV1GS8GQp#xk*xiogE zY!WI$&N7`KE$x^Rov^lrAG5fK+sXfuIL!;5couh+SLLXzi1NB7cEn{{A(Y^l zA|x9K#VIKwT`wT?sK8OBaNQTzrz1_4EDO{UXe0s=KBO@g zL3Xxt*Rm1U2UUVzk?1IR4vtzYukhPjpF6#;t?hAlZ|#e-F;G+a)jzu{!sq}1 literal 0 HcmV?d00001 diff --git a/backend/app/models/schemas/__pycache__/password.cpython-312.pyc b/backend/app/models/schemas/__pycache__/password.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..78872560819076904fbfa090a3103f4c53135941 GIT binary patch literal 1794 zcmbtU-D@0G6u)=AGWndONvc*8($Y50LsO6xOe}(0u?j9F#R$vdFwWeilg@tR-q|iI zfd(3(FGAkpiG%XI{;p@$7?nQ?V%_5p{>ZlcU{3guJ|mJRxx0q@7!|3 zz;~kxht;w7OKuRZW*#|l%@v|v;eOZUB?%;Gq?7hl80-*6^3*4JThibxs`0W1 zmF+$njIcUwA7F<>=^qJM>)n-Z-=J$*7g7(JEQ_^)9%nkt*0PgaC;K#KYaGK!Gp!+$ zdRVD5E_D&{pt`R})e8kjDhSMCZtnRim@CVLD@@#sD%HYYKW{$y=DWYYy7Tzkp9;lV z7>daCZeA$5V5`9e&Rj1ld=4&F_6u&cS}0-bL_v5rpcyz zjCG8$seVZNWmxIY*oI=Oa(tEXpcr^@;_pGtrRD+VY@MHkFZoLd7=yopa0=lygrjrJ z;uOJxS0JU}97Cg9A0~TnS2~!Z^YN zKq{e_CGuo%qCK2H({sj!{(G=@;vB#xX&Gc>^zPE(Bl@16c(et-VJ zZd!{E%*D7{K>GjgJJy50ihx01q`_(QAt1TMTL3_V6Vpv?`ta0DQ=36nf7aB`#;jKL zOS_^)!0d@o{elc{vgD+x&G1NZ}P10a_}h^mlUcF?pwB77%Q6rTNG+wo8S2NizEb^A^ zldEM+Tqfn5UTBYss)6nP24o8nl$S8d%kM*td4;G%!}3M25FI#$b6CwA#KdYHx&oaR z>LNrR==4w*B{7H_T9J9h1FIqO#UqG+@DOwrX2S~PFrJUWCXTF_IC@9hfjs==6GX)0 zxQNmFcmGFp>`-*Fre+ds=sNK$WzQ!ZoBDh-4TrO78tT&}4p@4nOw+dOrY~K4Z(#C; zH#7XI;}G9xMhU3BO0rDFc3?A&P+F!;EfElON`fl&>dC_wevtdn31=u;(t*O>pzbjs zTd)J`FoAV>U0PSZmHX4a0Yk2z6?gM$7bvj;TEWfhYNP4QV`Jj1>adCCEG9bA)Tw#SSqa6>MQFf4+Aszta+3dHV$M1 z9gLyW==S-$6W;~*rynMl9+*pAFrfc|B**euB+m?R>Lcl;#o(R?@|`0<#^Fu@Bpn70 zOe0(&U>ukRSW?~qONhk(^nCnC&qs}?A>c^Iuibs+-uV68!{p@$=4HyU$S7H6+~{pz zHX~=VnG`cE%d<<8{!jk}M z#Sdt2Dfml^4!VK8E;<3_8_HsiYx_Q|_F5U!G43|O35P<|CK{#@E^=bR*Z&L1r|3YI zHM3))#K`8=c09Ftr9D2mvDlbu;O6X}I@x~l#Kzl=`PNwT!k&7f{n8BBrW<~1q47y` zZu|Uh^z@!Ov#+H(Iy!c$J&|tDoH>X@w0EWbM6xyAT1(uXQa8v`r%t{U0UQl!>e-%&xzrgS)uFSmH z8||BP8vOX5gnt%DN0B7yCp5c@W*?(dKdZ6E!se9?=?{HMT9#UGY_EQ0fBw-^#3fPA E-|BIx*8l(j literal 0 HcmV?d00001 diff --git a/backend/app/models/schemas/__pycache__/role.cpython-312.pyc b/backend/app/models/schemas/__pycache__/role.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52017c51c6b73e50264051be1b4e3a5423778f2c GIT binary patch literal 7528 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 Mh1}=R-_DNWzr|%RI{*Lx literal 0 HcmV?d00001 diff --git a/backend/app/models/schemas/__pycache__/session.cpython-312.pyc b/backend/app/models/schemas/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..42bb7377f1142282eedeee75d787d86c85bf1ced GIT binary patch literal 1444 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 str: + if not v or len(v) > 50: + raise ValidationError("invalid_permission_name") + return v.strip() + + @field_validator("resource") + @classmethod + def resource_validator(cls, v: str) -> str: + if not v or len(v) > 30: + raise ValidationError("invalid_resource") + return v.strip() + + @field_validator("action") + @classmethod + def action_validator(cls, v: str) -> str: + if not v or len(v) > 20: + raise ValidationError("invalid_action") + return v.strip() + + +class PermissionCreate(PermissionBase): + pass + + +class PermissionUpdate(BaseModel): + name: str | None = None + description: str | None = None + resource: str | None = None + action: str | None = None + + @field_validator("name") + @classmethod + def name_validator(cls, v: str | None) -> str | None: + if v is None: + return v + if not v or len(v) > 50: + raise ValidationError("invalid_permission_name") + return v.strip() + + @field_validator("resource") + @classmethod + def resource_validator(cls, v: str | None) -> str | None: + if v is None: + return v + if not v or len(v) > 30: + raise ValidationError("invalid_resource") + return v.strip() + + @field_validator("action") + @classmethod + def action_validator(cls, v: str | None) -> str | None: + if v is None: + return v + if not v or len(v) > 20: + raise ValidationError("invalid_action") + return v.strip() + + +class PermissionPublic(PermissionBase): + id: int + + model_config = {"from_attributes": True, "arbitrary_types_allowed": True} + + +class RoleBase(BaseModel): name: RoleEnum description: str | None = None + @field_validator("name") + @classmethod + def name_validator(cls, v: RoleEnum) -> RoleEnum: + try: + return RoleEnum(v) + except ValueError: + raise ValidationError("invalid_role") -class RoleUpdate(BaseModel): - """ - Schema for updating a role - """ +class RoleCreate(RoleBase): + permissions: list[str] | None = None + + +class RoleUpdate(BaseModel): name: RoleEnum | None = None description: str | None = None + permissions: list[str] | None = None + + @field_validator("name") + @classmethod + def name_validator(cls, v: RoleEnum | None) -> RoleEnum | None: + if v is None: + return v + try: + return RoleEnum(v) + except ValueError: + raise ValidationError("invalid_role") + + +class RolePublic(RoleBase): + id: int + permissions: list[PermissionPublic] = [] + + model_config = {"from_attributes": True, "arbitrary_types_allowed": True} + + @field_validator("permissions", mode="before") + @classmethod + def permissions_from_obj(cls, v: Any) -> list[PermissionPublic]: + if isinstance(v, list): + result = [] + for item in v: + if isinstance(item, Permission): + result.append(PermissionPublic.model_validate(item)) + elif isinstance(item, dict): + result.append(PermissionPublic(**item)) + elif isinstance(item, PermissionPublic): + result.append(item) + return result + return [] + + def permission_names(self) -> list[str]: + return [p.name for p in self.permissions] + + +class RoleWithUsers(RolePublic): + user_count: int = 0 + + +class AssignRoleRequest(BaseModel): + user_id: str + role_id: int + + +class AssignPermissionsRequest(BaseModel): + role_id: int + permission_ids: list[int] diff --git a/backend/app/repositories/__pycache__/__init__.cpython-312.pyc b/backend/app/repositories/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e99cce7bc7af67176e3352575d0d0a75c86de487 GIT binary patch literal 184 zcmX@j%ge<81V%sZWrFC(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd8UiF-tYTbJi?d7e z3u2z^Xm~ky(bL}M=kqtmq?P966qh6>XX~aV0_lQG-J;aQ
qpipXFN=#xwK}=C< zL4I*&Nq$jgYH>__d}dx|NqoFsLFFwDo80`A(wtPgB37Ukj6hrrVtiy~WMnL22C@L& C^ffvF literal 0 HcmV?d00001 diff --git a/backend/app/repositories/__pycache__/base.cpython-312.pyc b/backend/app/repositories/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45b81910518c4f9296b5404afc2de01763aeac0b GIT binary patch literal 6988 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 literal 0 HcmV?d00001 diff --git a/backend/app/repositories/__pycache__/permission.cpython-312.pyc b/backend/app/repositories/__pycache__/permission.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfb03501c8d78fb4a2e8715330583dd1da4432dc GIT binary patch literal 8099 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�V|PmXJQGkaG1 zv;@7TVrWuat7xqu+M}i+a?$rjOT**vA-xr{o7B(QgZ@CKcm&I2kt*$B|s-jWTV=@N#%Ou0xC# zPaKyeU^{7Vt{(2n)I_%6)lH2js0LI?){Rw{Mzr6hlaS33zzUR=NozAos2;=6t0JvE z%Emlk)AD5CE6`_Fq(P$XfUXE@dSUYwi15U?q2UKj|A>}ODta1V0_!)ONhJjj1I$Ss z<6bNH<9pP2a#)J*&Gh_FO0!05AW@G(c8cU{YUg@>-m~l^ZJmpu9rK|bS04Y%(0fA* zq5TEoXzRF9*E%!D9LG&q+t=L+JnemQ+Dd3hcH<{spuC84ZsP z(^WJ8%|{zW8;JPILI}D_x(Xa*`ssboG7{Yk+5ooghOX80!A{vdreJZ4@B$P!eU_bJ zL&vXTF!E!}?6|-2yKFME>CU0>1W49T0HBJ-x?EejZtlH0c~!~PJ#vjdln-?*hPKUz zwq5DG+Px5ZXtD938UM#EZU02mUJxG63&ADMx!#-O8coZF?;DSRxmcmYoiNVA(}lLiu3pR}M#A$1=yY z1ZMgR0_og7)BlmM;fDQ*d#?#wKW^#DH@4;1cjiNz^DP^fJ>2@h%wb?|-}qbKuYFez z{`K&izJ>7Pi_MSE^yh0rIU#hTenY;wGvCm@%sCspGd_r=THD{~f3+X4-!&ol72Kq? zYl*O}7?4pGFA28f_~yb>3~4&Rd|TMZkd~v&%|Zakw{$f4Gts{7%zI4VX7=|7+)({s z7tL1?4^lfE7+=%@jMj6bvVh(nK*V%zc5TBe<@uVz@rdJe`X=y^mx*&?r zOvF`-;ub|Ioq$&gB!A*01zN!6QZE`1Y%$>1LZ%_+*<%Ycgc15oNUKnVPT{G$yNhR7xk~O3Hr8 zExrmgm(g|nUcB_wOI=aX1=S%}`Ey`&7~YFDd@G@EQ-|qmRncvtl2*J64tj?aS59B3 z_?{wu_G$@W#Ir>gAuBi;h5=PF>=I9ys_UfnI;s1dY`jkH{fyjoo!s*YdFVRX`l+XR zreV?3Jnw1FwftbgbN?yVvTKrIT3!jfIeEo@bvoC1c)sz`WrBTQ!ATlyU#RN%sX!<2 EU*TQNod5s; literal 0 HcmV?d00001 diff --git a/backend/app/repositories/__pycache__/unit_of_work.cpython-312.pyc b/backend/app/repositories/__pycache__/unit_of_work.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..800bc7fe4fb513deed20174e4ccf3dd55fa4ef61 GIT binary patch literal 4459 zcmd59TWnlMb>=>H@4l|>jbHIYStm}aZSdyNQs>c9IS)cnedDh@d}c(=CFAesIp*-Sw^= zQIPmxqz-Pb9Sb+Tz2esy_B9*@aa+8=ONla==7EMy)Z$|1oT@pD#O#KQmTW(9$b=|N| zWA%L|sM*SbW*d`P_f}w;P|WBRHMzAY#kZ7R3BHlbII;Xh?9Dts8!H%&!+0*1$rdb@ zWjV)iGWlF=BF|%4BWFxza+5LBaEx)oW-*5wIon7hhn)z1gnu86Q&%6ci@Ed(mSfy- z@;olN@<^`e`cD@N>>CCL4*xM$U^&wUG%T1;G24b+U>urcPRgzi+fOo{&4|g@Ss`y{ zV8bH*3cb(tF~8|A`3W&Krd6>33sf=1^iODJ5IBQo ziy4AG1brC#FpHRq*e`NLxn2EfQZM@Bt*%hd*CtRorLPBL(EeBY!o6I7Rn+OK?TDYt z0P(B*48Tn?OJ_++OdxP@Mzq>VVYa^7f!lAIRcbURRKpt9*8Gn+El-)P>3}%V+U!(E z4O{O`TJs1>z^>)emxZkGj<0}vGlrEZfx(_(G1NAiJ8C;NlX1?(jCG5yN+VHUT?qq(_n2j$4k0ebt{`=5dd?f ztdXs;tIFGv`yD%$J9YvVyx$#P?%utD*1Ko9ci&ZIMeDw==?mfc@M6cF-)eg+unBQ} zoH^4xm##)QqzOyA41MJQfL93xi$j0&+Em@zY83FU23^mRQ{+Q=hQ_7R{<_kR=OBg& zWg8fYV(B~(E;wEUW4uAT{el&PT9cZq3*r**g5UCP03T3}vD3zr$9NAwH<(Hp5Fr@{ z;dL|#uSVX5E0Fgus~TTrc~HqL#z4aWsvf>LH6%R zH|6iDDrluhi*%U0?Gw_LOZ(QPeE}Q_J?nFXJzDBLej&{YBB5k*O%gv8i_1dRu7Aeh zIZ%kJfyY>A#&JbRo)5rL`E~?500_CEdwaSFpf2Dbpo;blsu0v6TyhYA&_p>(!tINp zZTG{`h0*!Z#r^|#y6yxPdylMykFG^KZw}oU`u?8P@W4`dpsbQ`>|uD|iH$CPDRGeg zm<}sH@gJm5H{!Q|>%TYRsYV6E4%nvZq0}2AY*3H1K|K;EN8O~Hqxo#Ms-E;T=U}dl zS-Zg#Iz3JYeW4+*kAn2;KEN>qyS}v8lW&7TgFEmzj7x0|!uR+NA=_>M8+~ACMN6#d z(S_`Mc2(cLq;Fr=2UoShd)nZWijehlFVUaV!^$swFa1Bu`nkFzKuIDDRyf1Imv%Su zqNvfppDOR2ATc3tjzMITu#wmlv1{If!Fr>(^?7}jjP3DYI4kK>`gBMf)WD5F??G%aA()#sJFfR<$ZeA}FHzn0L^=`J&f-vcoq74>Q zUHKKD+d2TqLP65nzHo8=;-c2a`+x*7x*s}@wL=vjjkjQ0%NbdgN@0}F=FMpf@o*}2 zZrZS_GyYV{%%@W+j*iAL)$terSHU3c2I|3zqjU0S5TK0&N91X`1G=*sQtJ4J|@srWFS07f_Ea6 zi#VbXQcujA+767m?H9vLDTGe{)X+59b54C?x z#+aBCZ6EDDvIN?va<}y9emVVc*?@KEz*Ef&Obn2F`d7-wRv0eyo(SGH6Ld z@MP6zr!k)9cy-qYUJJ9Hv!ZFc{%T3g@rB`b+UG38N}plbVgic6gs4_BdExwS2p@&% zJZA}WoTL4@y_=qi>s7l~#&>MufEdiYa=&}iTs4$=kAuxi@#P>M5gcvT4S@b$6AuF~ z`wA~cUGnDXhxP6Hq2O4^>2smY#QVKDLDiw{;mIvh7-;-^RNbdu(=MmZQ zfV}vC?0rC90Qd(bboJ=v*XHP2pyz7WYM^H+(6iWkXgP3r&i8lUFztU#5S*u^fAGCm fm;3fUmUXJHMYfi)f2eM`{?#RQ+b1dv#IgPbPGcFn literal 0 HcmV?d00001 diff --git a/backend/app/repositories/__pycache__/user.cpython-312.pyc b/backend/app/repositories/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d993906d192b5dd219ad55872440eb9f2881f2d GIT binary patch literal 10347 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 literal 0 HcmV?d00001 diff --git a/backend/app/repositories/permission.py b/backend/app/repositories/permission.py new file mode 100644 index 00000000..a2eac8bb --- /dev/null +++ b/backend/app/repositories/permission.py @@ -0,0 +1,137 @@ +""" +Permission repository for the application. +""" + +import logging + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.models.domain.permission import Permission, PermissionEnum +from app.models.schemas.role import PermissionCreate, PermissionUpdate +from app.repositories.base import BaseRepository + +logger = logging.getLogger("app.repositories.permission") + + +class PermissionRepository(BaseRepository[Permission, PermissionCreate, PermissionUpdate]): + """ + Repository for managing permissions. + """ + + def __init__(self) -> None: + """ + Initialize permission repository. + """ + super().__init__(Permission) + + async def get_by_name( + self, + session: AsyncSession, + name: str, + ) -> Permission | None: + """ + Get permission by name. + """ + logger.debug(f"Getting permission by name: {name}") + query = select(self.model).where(self.model.name == name) + result = await session.execute(query) + return result.scalars().first() + + async def get_by_ids( + self, + session: AsyncSession, + permission_ids: list[int], + ) -> list[Permission]: + """ + Get permissions by list of IDs. + """ + logger.debug(f"Getting permissions by IDs: {permission_ids}") + if not permission_ids: + return [] + query = select(self.model).where(self.model.id.in_(permission_ids)) + result = await session.execute(query) + return list(result.scalars().all()) + + async def get_by_names( + self, + session: AsyncSession, + permission_names: list[str], + ) -> list[Permission]: + """ + Get permissions by list of names. + """ + logger.debug(f"Getting permissions by names: {permission_names}") + if not permission_names: + return [] + query = select(self.model).where(self.model.name.in_(permission_names)) + result = await session.execute(query) + return list(result.scalars().all()) + + async def get_all( + self, + session: AsyncSession, + ) -> list[Permission]: + """ + Get all permissions. + """ + logger.debug("Getting all permissions") + query = select(self.model).order_by(self.model.id) + result = await session.execute(query) + return list(result.scalars().all()) + + async def get_by_resource( + self, + session: AsyncSession, + resource: str, + ) -> list[Permission]: + """ + Get permissions by resource. + """ + logger.debug(f"Getting permissions by resource: {resource}") + query = select(self.model).where(self.model.resource == resource).order_by(self.model.id) + result = await session.execute(query) + return list(result.scalars().all()) + + async def initialize_permissions(self, session: AsyncSession) -> None: + """ + Initialize all default permissions. + """ + logger.info("Initializing default permissions") + + permissions_config = [ + (1, PermissionEnum.USER_READ, "Read user information", "user", "read"), + (2, PermissionEnum.USER_CREATE, "Create new users", "user", "create"), + (3, PermissionEnum.USER_UPDATE, "Update user information", "user", "update"), + (4, PermissionEnum.USER_DELETE, "Delete users", "user", "delete"), + (5, PermissionEnum.ROLE_READ, "Read role information", "role", "read"), + (6, PermissionEnum.ROLE_CREATE, "Create new roles", "role", "create"), + (7, PermissionEnum.ROLE_UPDATE, "Update role information", "role", "update"), + (8, PermissionEnum.ROLE_DELETE, "Delete roles", "role", "delete"), + (9, PermissionEnum.PERMISSION_READ, "Read permission information", "permission", "read"), + (10, PermissionEnum.PERMISSION_CREATE, "Create new permissions", "permission", "create"), + (11, PermissionEnum.PERMISSION_UPDATE, "Update permission information", "permission", "update"), + (12, PermissionEnum.PERMISSION_DELETE, "Delete permissions", "permission", "delete"), + (13, PermissionEnum.ENDPOINT_EXECUTE, "Execute API endpoints", "endpoint", "execute"), + (14, PermissionEnum.ENDPOINT_MANAGE, "Manage API endpoints", "endpoint", "manage"), + ] + + for perm_id, perm_name, description, resource, action in permissions_config: + existing = await self.get_by_name(session, perm_name) + if not existing: + permission = Permission( + id=perm_id, + name=perm_name, + description=description, + resource=resource, + action=action, + ) + session.add(permission) + logger.info(f"Created permission: {perm_name}") + + await session.flush() + logger.info("Permissions initialized successfully") + + +permission_repo = PermissionRepository() diff --git a/backend/app/repositories/role.py b/backend/app/repositories/role.py index 4f2b3415..786943f1 100644 --- a/backend/app/repositories/role.py +++ b/backend/app/repositories/role.py @@ -4,12 +4,16 @@ import logging -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload +from app.models.domain.permission import Permission from app.models.domain.role import Role, RoleEnum +from app.models.domain.user import User from app.models.schemas.role import RoleCreate, RoleUpdate from app.repositories.base import BaseRepository +from app.repositories.permission import permission_repo logger = logging.getLogger("app.repositories.role") @@ -31,22 +35,74 @@ async def get_by_name( name: RoleEnum, ) -> Role | None: """ - Get role by name. + Get role by name with permissions loaded. """ logger.debug(f"Getting role by name: {name}") - query = select(self.model).where(self.model.name == name) + query = ( + select(self.model) + .options(selectinload(self.model.permissions)) + .where(self.model.name == name) + ) result = await session.execute(query) return result.scalars().first() + async def get_by_id_with_permissions( + self, + session: AsyncSession, + role_id: int, + ) -> Role | None: + """ + Get role by ID with permissions loaded. + """ + logger.debug(f"Getting role by ID with permissions: {role_id}") + query = ( + select(self.model) + .options(selectinload(self.model.permissions)) + .where(self.model.id == role_id) + ) + result = await session.execute(query) + return result.scalars().first() + + async def get_all_with_permissions( + self, + session: AsyncSession, + ) -> list[Role]: + """ + Get all roles with permissions loaded. + """ + logger.debug("Getting all roles with permissions") + query = ( + select(self.model) + .options(selectinload(self.model.permissions)) + .order_by(self.model.id) + ) + result = await session.execute(query) + return list(result.scalars().unique().all()) + + async def get_role_user_count( + self, + session: AsyncSession, + role_id: int, + ) -> int: + """ + Get the number of users assigned to a role. + """ + logger.debug(f"Getting user count for role: {role_id}") + query = select(func.count(User.id)).where(User.role_id == role_id) + result = await session.execute(query) + count = result.scalar() + return count if count is not None else 0 + async def _create_new_role( self, session: AsyncSession, name: RoleEnum, description: str | None, role_id: int | None = None, + permissions: list[Permission] | None = None, ) -> Role: """ - Create new role with optional specific ID. + Create new role with optional specific ID and permissions. """ logger.info(f"Creating new role: {name}") @@ -54,6 +110,8 @@ async def _create_new_role( role = Role(id=role_id, name=name) # type: ignore[call-arg] if description is not None: role.description = description + if permissions: + role.permissions = permissions session.add(role) await session.flush() await session.refresh(role) @@ -61,15 +119,73 @@ async def _create_new_role( else: role_data = RoleCreate(name=name, description=description) role = await self.create(session=session, obj_in=role_data) + if permissions: + role.permissions = permissions + await session.flush() + await session.refresh(role) + + return role + + async def create_role_with_permissions( + self, + session: AsyncSession, + obj_in: RoleCreate, + ) -> Role: + """ + Create a new role with permissions. + """ + logger.info(f"Creating role with permissions: {obj_in.name}") + + permissions = [] + if obj_in.permissions: + permissions = await permission_repo.get_by_names(session, obj_in.permissions) + + role = Role( + name=obj_in.name, + description=obj_in.description, + ) + if permissions: + role.permissions = permissions + + session.add(role) + await session.flush() + await session.refresh(role) return role + async def update_role_with_permissions( + self, + session: AsyncSession, + db_obj: Role, + obj_in: RoleUpdate, + ) -> Role: + """ + Update role with permissions. + """ + logger.debug(f"Updating role: {db_obj.id}") + + update_data = obj_in.model_dump(exclude_unset=True, exclude={"permissions"}) + + for field, value in update_data.items(): + setattr(db_obj, field, value) + + if obj_in.permissions is not None: + permissions = await permission_repo.get_by_names(session, obj_in.permissions) + db_obj.permissions = permissions + + session.add(db_obj) + await session.flush() + await session.refresh(db_obj) + + return db_obj + async def get_or_create( self, session: AsyncSession, name: RoleEnum, description: str | None = None, role_id: int | None = None, + permissions: list[Permission] | None = None, ) -> Role: """ Get role by name or create if it doesn't exist. @@ -83,7 +199,7 @@ async def get_or_create( description, ) - return await self._create_new_role(session, name, description, role_id) + return await self._create_new_role(session, name, description, role_id, permissions) async def _update_role_description( self, @@ -100,25 +216,135 @@ async def _update_role_description( role = await self.update(session=session, db_obj=role, obj_in=update_data) return role + async def assign_permissions_to_role( + self, + session: AsyncSession, + role_id: int, + permission_ids: list[int], + ) -> Role | None: + """ + Assign permissions to a role. + """ + logger.info(f"Assigning permissions {permission_ids} to role {role_id}") + + role = await self.get_by_id_with_permissions(session, role_id) + if not role: + return None + + permissions = await permission_repo.get_by_ids(session, permission_ids) + role.permissions = permissions + + session.add(role) + await session.flush() + await session.refresh(role) + + return role + + async def add_permission_to_role( + self, + session: AsyncSession, + role_id: int, + permission_id: int, + ) -> Role | None: + """ + Add a single permission to a role. + """ + logger.info(f"Adding permission {permission_id} to role {role_id}") + + role = await self.get_by_id_with_permissions(session, role_id) + if not role: + return None + + permission = await permission_repo.get(session, permission_id) + if not permission: + return None + + if permission not in role.permissions: + role.permissions.append(permission) + session.add(role) + await session.flush() + await session.refresh(role) + + return role + + async def remove_permission_from_role( + self, + session: AsyncSession, + role_id: int, + permission_id: int, + ) -> Role | None: + """ + Remove a permission from a role. + """ + logger.info(f"Removing permission {permission_id} from role {role_id}") + + role = await self.get_by_id_with_permissions(session, role_id) + if not role: + return None + + permission = await permission_repo.get(session, permission_id) + if not permission: + return None + + if permission in role.permissions: + role.permissions.remove(permission) + session.add(role) + await session.flush() + await session.refresh(role) + + return role + async def initialize_roles(self, session: AsyncSession) -> None: """ - Initialize all roles with descriptions and specific IDs. + Initialize all roles with descriptions, specific IDs, and default permissions. """ - logger.info("Initializing roles with specific IDs") + logger.info("Initializing roles with specific IDs and permissions") + + all_permissions = await permission_repo.get_all(session) + permission_map = {p.name: p for p in all_permissions} + + admin_permissions = list(all_permissions) + + manager_permission_names = [ + "user:read", + "user:create", + "user:update", + "role:read", + "endpoint:execute", + ] + manager_permissions = [ + permission_map[name] for name in manager_permission_names if name in permission_map + ] + + user_permission_names = [ + "user:read", + "endpoint:execute", + ] + user_permissions = [ + permission_map[name] for name in user_permission_names if name in permission_map + ] + + guest_permission_names = [ + "endpoint:execute", + ] + guest_permissions = [ + permission_map[name] for name in guest_permission_names if name in permission_map + ] roles_config = [ - (1, RoleEnum.ADMIN, "Administrator with full access"), - (2, RoleEnum.MANAGER, "Manager with limited administrative access"), - (3, RoleEnum.USER, "Regular user"), - (4, RoleEnum.GUEST, "Guest user with restricted access"), + (1, RoleEnum.ADMIN, "Administrator with full access", admin_permissions), + (2, RoleEnum.MANAGER, "Manager with limited administrative access", manager_permissions), + (3, RoleEnum.USER, "Regular user", user_permissions), + (4, RoleEnum.GUEST, "Guest user with restricted access", guest_permissions), ] - for role_id, role_name, description in roles_config: + for role_id, role_name, description, permissions in roles_config: await self.get_or_create( session=session, name=role_name, description=description, role_id=role_id, + permissions=permissions, ) logger.info("Roles initialized successfully") diff --git a/backend/app/repositories/unit_of_work.py b/backend/app/repositories/unit_of_work.py index 217cab15..cfbccd01 100644 --- a/backend/app/repositories/unit_of_work.py +++ b/backend/app/repositories/unit_of_work.py @@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core.db import get_session +from app.repositories.permission import PermissionRepository from app.repositories.role import RoleRepository from app.repositories.session import SessionRepository from app.repositories.user import UserRepository @@ -31,6 +32,7 @@ def __init__(self, session: AsyncSession): self.users = UserRepository() self.sessions = SessionRepository() self.roles = RoleRepository() + self.permissions = PermissionRepository() logger.debug("Unit of Work initialized") async def __aenter__(self) -> "UnitOfWork": diff --git a/backend/app/services/__pycache__/__init__.cpython-312.pyc b/backend/app/services/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..142dcf26e29ee147ce6cf0c85193f21b22e5ee98 GIT binary patch literal 180 zcmX@j%ge<81V%sZWrFC(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd8UQ3*tYTbJi?d7e z3u2z^Xm~ky(bL}M=kqtmq?P966qh6>XX~aV0_lQG-J;aQ
qpipXFN=#xwK}>OK yQCVhkYH>__d}dx|NqoFsLFFwDo80`A(wtPgB37XBj6hrrVtiy~WMnL22C@L;;4^Xn literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/auth_service.cpython-312.pyc b/backend/app/services/__pycache__/auth_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f22dd0b04d3a4dde8baff877d705d9a198d3296d GIT binary patch literal 12798 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 literal 0 HcmV?d00001 diff --git a/backend/app/services/__pycache__/user_service.cpython-312.pyc b/backend/app/services/__pycache__/user_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bb94192b81af6e8285758c4a6868112a4ca833b GIT binary patch literal 9207 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
qpipXFN=#xwK}=~$ vW=?TTe0*kJW=VX!UP0w84x8Nkl+v73yCPPg*^EG33}Sp_W@Kb6Vg|ARG!`;E literal 0 HcmV?d00001 diff --git a/backend/app/utils/__pycache__/auth.cpython-312.pyc b/backend/app/utils/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..099972cd0407056136d3128bd1d9ab0227706331 GIT binary patch literal 4182 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 Z`L}I}7xpDL?@ze)CoSC%EWp6D{x2FZpw0jQ literal 0 HcmV?d00001 diff --git a/backend/app/utils/__pycache__/notification.cpython-312.pyc b/backend/app/utils/__pycache__/notification.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d64c587c1525fc97fd0723e6fe54fcf3c9c459d GIT binary patch literal 2948 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 literal 0 HcmV?d00001 diff --git a/backend/app/validators/__pycache__/__init__.cpython-312.pyc b/backend/app/validators/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7030ac7372f999fc0ac0ae74a43399b19affc511 GIT binary patch literal 182 zcmX@j%ge<81V%sZWrFC(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxd8U!RXX~aV0_lQG-J;aQ
qpipXFN=#xwK}=a< zPG(ACNq$jrOniK1US>&ryk0@&Ee@O9{FKt1RJ$TppaqOTTnu7-WM*V!EMf+-0A3X| ANdN!< literal 0 HcmV?d00001 diff --git a/backend/app/validators/__pycache__/user.cpython-312.pyc b/backend/app/validators/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9090441b50a90b47aaae1532c327880a2489b8f2 GIT binary patch literal 3297 zcmb_e-ER{|5Z}A=*^ceRjtNZ&A2nYU7i9DODX3bB&_1Am8i79nj_~A_Rb`+ zjD#o;psGS@D-o$`i~0nj50(1T|Di7~AQhcZ1u9jQHzx*x`qY`dJNpQ!)Q7Hgv%9mi zbGtLYnb}`@dlLlO?w7xti*Z8!z)8>|{-9lj!A)Y2A~6I*w1lDny<~|csVJ2q#Yjmm z%B5&AD&Ts=ik0HUxIjem9x>#r#E9M%Jx#?P&Wiyr-r*%UuLpRE4lg+;D#`lR#A%zd z+(peY4b7>r+$4O?6wPVXs%7e$V^+$A!~*IbQzCA5w4|BVSj9Pm8%7zcFgL?_<8x(4 zoAo$-d{uL(!I#`VUp-YRZdU|1?Mr}mzD4&A?@`&Gv`1iYlTZS-AVtv-iV}?&B9#q^ z%99Zzay4?-7cEBTWF_kM<7LWPiTdg*tbQ<&gVvez`_+6YoRl;tr%NKkKhbd#eC=<6fa3&5;Cvtr;jTZ{#aVHBm=KmWcyA9nxQ91G z3Z$+G0$G{w1g0S8qrtvzZ$#iv{+aCL${HHNI8o&KetxBpC1@_VUDyZ~Eq~%ndG|-* z1w8G-01!80mbn+zU|&7`@kd&|{%*dwGk-`uyL}75LjnaXhT;J zJnIQaKMZUI$W^i^igLUaC(-_vn2?8VNi721ZD|R+wgmh|IPhl3tn?w75tK;%(CZ=c+EvP!FD$c8bEeaclH-uj2J|Va zPpbyC%!`z1=S<6V=G@*OXt*XtW_T<&(NSYC3^0br=3%2qY!!|-qF6dWt>L9j8)YFV zJy;-4>Vu=0PdoPS;2cq6j{-rQ$9ncWDja#3S^aS9$TL~m(Rb}6Kxbg{Z>#dZuF5x4 z`InC}n;vZ0+f46UA_53!H{RNDKfAk`+TD_1{5#bU~WK zDUvPltpL)byk~yAMWDMizJ#3*QW$w!aPKQ14cyf}0W1Ht2tav|p*%$4i}__DZp1@n zA~x5fB;5W}5OgyYW^^o9KfD5xtG-l@(F-+`(GrE?z>7t#3dNFk3Pm9JI;_!U__Yu6 zTZBP<*RK(VFb!~K;ruNIYgYuGza{^8;QXMH(OmG;oR_ryB37zebQYl3Mdt!~ubaMu zZHmY;u;9T?^kBzfB!=pbx9u0l@o5R;oR zovECEyRHWzW0;ej$Cjr8;v7AJZzaAw*R@h6u%243II2w@H*1#l3RqSTS+>{4Diny~ z+YKsbq2`#DT?n{r7yN=lfY%*JP(1zQky}8r0|^S*Xcm4lj*M<{QoWniH=g=el=vdY zQO!vpm&x-1(sSTRdi|qN#eI_A(0p^pj~gDO-{I{2_p=9@sRNIbYv&vHlG}k7WTnUH z?2X3t#{7xb1X#c&iSYu2@TaFig4-lZgr{HzBf zFYDo7m6Uf { + const navigate = useNavigate(); + const location = useLocation(); const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); const user = useAppSelector((state) => state.user); + const isAdmin = user.role === "ADMIN"; const { refetch: refetchUserData } = useGetUserMeQuery(undefined, { skip: !isAuthenticated, @@ -29,13 +33,58 @@ const Header: React.FC = () => { } }; + const isRolesPage = location.pathname === "/roles"; + const isHomePage = location.pathname === "/"; + return ( Fullstack FastAPI Template + + + + {isAdmin && ( + + )} + + + + + + {isAdmin && ( + + )} + + diff --git a/frontend/src/components/RoleDetail.tsx b/frontend/src/components/RoleDetail.tsx new file mode 100644 index 00000000..46690abf --- /dev/null +++ b/frontend/src/components/RoleDetail.tsx @@ -0,0 +1,291 @@ +import { + Box, + Button, + Card, + Flex, + Heading, + Text, + Input, + Textarea, + Field, + Select, + Checkbox, + Spinner, + Badge, + Divider, + IconButton, + Tooltip, +} from "@chakra-ui/react"; +import { useState, useEffect } from "react"; +import { LuPlus, LuX, LuSave, LuShield } from "react-icons/lu"; +import { + useGetRoleByIdQuery, + useUpdateRoleMutation, + useGetPermissionsQuery, + useCreateRoleMutation, +} from "@/redux/services/roleApi"; +import type { Role, RoleUpdate, RoleCreate } from "@/types/role"; +import { toaster } from "@/components/ui/toaster"; +import { TOAST_DURATION } from "@/config/env"; + +interface RoleDetailProps { + roleId: number | null; + onClose: () => void; + onSaved: () => void; +} + +const RoleDetail: React.FC = ({ roleId, onClose, onSaved }) => { + const isNewRole = roleId === null; + const { data: role, isLoading: roleLoading } = useGetRoleByIdQuery(roleId!, { + skip: isNewRole, + }); + const { data: permissions, isLoading: permissionsLoading } = useGetPermissionsQuery({ + skip: 0, + limit: 100, + }); + const [updateRole] = useUpdateRoleMutation(); + const [createRole] = useCreateRoleMutation(); + + const [formData, setFormData] = useState<{ + name: RoleCreate["name"]; + description: string; + selectedPermissions: number[]; + }>({ + name: "USER", + description: "", + selectedPermissions: [], + }); + + const [isSaving, setIsSaving] = useState(false); + + useEffect(() => { + if (role) { + setFormData({ + name: role.name, + description: role.description || "", + selectedPermissions: role.permissions.map((p) => p.id), + }); + } + }, [role]); + + const handlePermissionToggle = (permissionId: number) => { + setFormData((prev) => ({ + ...prev, + selectedPermissions: prev.selectedPermissions.includes(permissionId) + ? prev.selectedPermissions.filter((id) => id !== permissionId) + : [...prev.selectedPermissions, permissionId], + })); + }; + + const handleSave = async () => { + setIsSaving(true); + try { + const permissionNames = permissions + ?.filter((p) => formData.selectedPermissions.includes(p.id)) + .map((p) => p.name); + + if (isNewRole) { + const roleCreate: RoleCreate = { + name: formData.name, + description: formData.description || null, + permissions: permissionNames, + }; + await createRole(roleCreate).unwrap(); + toaster.create({ + title: "Role created", + description: "Role has been created successfully", + type: "success", + duration: TOAST_DURATION, + }); + } else { + const roleUpdate: RoleUpdate = { + name: formData.name, + description: formData.description || null, + permissions: permissionNames, + }; + await updateRole({ + roleId: roleId!, + roleData: roleUpdate, + }).unwrap(); + toaster.create({ + title: "Role updated", + description: "Role has been updated successfully", + type: "success", + duration: TOAST_DURATION, + }); + } + onSaved(); + } catch (err: unknown) { + const errorMessage = + (err as { data?: { message?: string } })?.data?.message || + "Failed to save role"; + toaster.create({ + title: "Error", + description: errorMessage, + type: "error", + duration: TOAST_DURATION, + }); + } finally { + setIsSaving(false); + } + }; + + const getPermissionsByResource = () => { + if (!permissions) return {}; + return permissions.reduce((acc, perm) => { + if (!acc[perm.resource]) { + acc[perm.resource] = []; + } + acc[perm.resource].push(perm); + return acc; + }, {} as Record); + }; + + if (roleLoading && !isNewRole) { + return ( + + + + ); + } + + const permissionsByResource = getPermissionsByResource(); + + return ( + + + + + + {isNewRole ? "Create New Role" : `Edit Role: ${role?.name}`} + + + + + + + + + + + + Role Information + + + + + Role Name + + setFormData((prev) => ({ + ...prev, + name: e.value as RoleCreate["name"], + })) + } + disabled={!isNewRole} + > + + + + + ADMIN + MANAGER + USER + GUEST + + + + + + Description +