feat(org): panel-based access system replacing roles (procurement/inventory)
- new tables user_panels (panel: procurement/inventory/pm + level full/limited)
and user_project_members (M:N membership, separate from pm responsibility)
- UserContext carries panels + member_project_ids; /users/me and the users
list include them
- admin endpoints: PUT /users/{id}/panels, PUT /users/{id}/panel-projects
- inventory scope: panel inventory 'full' -> manager scope (all projects, NO
stock change); 'limited' -> supervisor scope (own memberships, stock change
allowed); legacy roles kept as migration fallback; admin untouched
- procurement writes: _procurement_writer is now panel-aware (admin, legacy
procurement_manager, or procurement 'full') across all 18 write routers;
quote CREATE additionally allows 'limited' users restricted to their member
projects (validated server-side)
- frontend: panel-aware HomeRoute (inventory -> warehouse panel, procurement
limited -> minimal RequesterHome), wizard project options restricted for
limited users, change-inventory button/route gated to inventory 'limited',
organization panel gains the panels & access-levels management section
This commit is contained in:
+55
-1
@@ -27,7 +27,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.config import settings
|
||||
from app.db.engine import AsyncSessionLocal
|
||||
from app.models.procurement import Project, User
|
||||
from app.models.procurement import Project, User, UserPanel, UserProjectMembership
|
||||
from app.utils.errors import (
|
||||
ForbiddenError,
|
||||
ServiceUnavailableError,
|
||||
@@ -106,6 +106,10 @@ class UserContext:
|
||||
project_id: object | None # uuid.UUID | None — default (first) project of a PM
|
||||
project_ids: list # every project this PM manages (1:N — empty for non-PMs)
|
||||
display_name: str
|
||||
# لایه پنلها (جایگزین نقشها): {'procurement': 'full'|'limited', 'inventory': ...}
|
||||
panels: dict | None = None
|
||||
# عضویت M:N در پروژهها — محدوده داده سطح limited (جدا از مسئولیت PM)
|
||||
member_project_ids: list | None = None
|
||||
|
||||
|
||||
async def context_from_claims(claims: dict, db: AsyncSession) -> UserContext:
|
||||
@@ -153,6 +157,26 @@ async def context_from_claims(claims: dict, db: AsyncSession) -> UserContext:
|
||||
project_ids = [p.id for p in projects]
|
||||
project_id = project_ids[0] if project_ids else None
|
||||
|
||||
# لایه پنلها: تخصیص پنل + سطح دسترسی و عضویت پروژهای (جدا از نقش)
|
||||
panels: dict = {}
|
||||
panel_rows = (
|
||||
await db.execute(
|
||||
select(UserPanel).where(UserPanel.user_id == user.id).order_by(UserPanel.created_at)
|
||||
)
|
||||
).scalars().all()
|
||||
for row in panel_rows:
|
||||
panels[row.panel] = row.level
|
||||
member_project_ids: list = [
|
||||
row.project_id
|
||||
for row in (
|
||||
await db.execute(
|
||||
select(UserProjectMembership)
|
||||
.where(UserProjectMembership.user_id == user.id)
|
||||
.order_by(UserProjectMembership.created_at)
|
||||
)
|
||||
).scalars().all()
|
||||
]
|
||||
|
||||
display_name = f"{user.first_name} {user.last_name}".strip() or user.casdoor_id
|
||||
return UserContext(
|
||||
user=user,
|
||||
@@ -160,6 +184,8 @@ async def context_from_claims(claims: dict, db: AsyncSession) -> UserContext:
|
||||
project_id=project_id,
|
||||
project_ids=project_ids,
|
||||
display_name=display_name,
|
||||
panels=panels,
|
||||
member_project_ids=member_project_ids,
|
||||
)
|
||||
|
||||
|
||||
@@ -222,3 +248,31 @@ def require_role(*roles: str):
|
||||
raise ForbiddenError("دسترسی به این بخش برای نقش شما مجاز نیست")
|
||||
return ctx
|
||||
return _dep
|
||||
|
||||
|
||||
def require_procurement_writer():
|
||||
"""نوشتن در پنل تدارکات — ادمین، پنل تدارکات 'full'، یا نقش میراث مدیر تدارکات
|
||||
(دوره مهاجرت از مدل نقش به مدل پنل)."""
|
||||
|
||||
async def _dep(ctx: UserContext = Depends(get_current_user)) -> UserContext:
|
||||
if ctx.role in ("admin", "procurement_manager"):
|
||||
return ctx
|
||||
if (ctx.panels or {}).get("procurement") == "full":
|
||||
return ctx
|
||||
raise ForbiddenError("دسترسی نوشتن در پنل تدارکات را ندارید")
|
||||
|
||||
return _dep
|
||||
|
||||
|
||||
def allow_quote_create():
|
||||
"""افزودن استعلام — علاوه بر «نوشتن»، پنل تدارکات 'limited' هم مجاز است
|
||||
(محدود به پروژههای عضویت؛ اعتبارسنجی عضویت در خود endpoint انجام میشود)."""
|
||||
|
||||
async def _dep(ctx: UserContext = Depends(get_current_user)) -> UserContext:
|
||||
if ctx.role in ("admin", "procurement_manager"):
|
||||
return ctx
|
||||
if (ctx.panels or {}).get("procurement") in ("full", "limited"):
|
||||
return ctx
|
||||
raise ForbiddenError("دسترسی افزودن استعلام ندارید")
|
||||
|
||||
return _dep
|
||||
|
||||
Reference in New Issue
Block a user