revert: restore production to the state before 730d0bd (PM/RBAC work moved to 'pm')

Their three commits (730d0bd, 5a9b543, 387edfb) reached production by push and
auto-deployed. 387edfb expects a users.username column that the live database does
not have, so POST /api/v1/auth/login was returning HTTP 500. Their schema changes
never applied either (projects still has only id, name, description, start_date,
pm_user_id, created_at), so nothing depends on the reverted code.

This commit sets the tree back to exactly a02b3ea — verified with git diff against
it — while keeping history linear: the work itself is preserved on 'pm' at 387edfb.
This commit is contained in:
ZCode
2026-09-26 17:41:07 +00:00
parent 387edfb1ab
commit 253c922280
56 changed files with 753 additions and 1479 deletions
+14 -47
View File
@@ -17,7 +17,7 @@ import uuid
import httpx
from fastapi import APIRouter, Depends
from sqlalchemy import select, update, text
from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.auth import UserContext, get_current_user, require_role
@@ -41,8 +41,6 @@ USER_ROLE_LABELS = {
"pm": "مدیر پروژه",
"procurement_manager": "مدیر تدارکات",
"admin": "ادمین",
# کاربر فقطِ پنل مدیریت پروژه (PM) — به انبار/تدارکات دسترسی ندارد
"supervisor": "سرپرست پروژه",
}
@@ -54,7 +52,6 @@ def user_to_dict(user: User, projects: list[Project] | None = None) -> dict:
return {
"id": str(user.id),
"casdoor_id": user.casdoor_id,
"username": user.username,
"first_name": user.first_name,
"last_name": user.last_name,
"phone": user.phone,
@@ -68,15 +65,15 @@ def user_to_dict(user: User, projects: list[Project] | None = None) -> dict:
async def _org_manager(ctx: UserContext = Depends(get_current_user)) -> UserContext:
"""Admin, procurement manager, or project manager — organization management."""
if ctx.role not in ("admin", "procurement_manager", "pm"):
"""Admin OR procurement manager (the same person) — organization management."""
if ctx.role not in ("admin", "procurement_manager"):
raise ForbiddenError("دسترسی به این بخش برای نقش شما مجاز نیست")
return ctx
async def _user_projects(db: AsyncSession, user: User) -> list[Project]:
"""Every project assigned to the user (supervisor or PM) (oldest first)."""
if user.role not in ("supervisor", "pm"):
"""Every project the user manages as PM (oldest first; [] for non-PMs)."""
if user.role != "pm":
return []
rows = (
await db.execute(
@@ -151,13 +148,13 @@ async def list_users(
):
"""Every user (with role + assigned projects) — Organization panel."""
users = (await db.execute(select(User).order_by(User.created_at))).scalars().all()
supervisor_ids = {u.id for u in users if u.role in ("supervisor", "pm")}
pm_ids = {u.id for u in users if u.role == "pm"}
by_user: dict = {}
if supervisor_ids:
if pm_ids:
projs = (
await db.execute(
select(Project)
.where(Project.pm_user_id.in_(supervisor_ids))
.where(Project.pm_user_id.in_(pm_ids))
.order_by(Project.created_at, Project.id)
)
).scalars().all()
@@ -207,7 +204,7 @@ async def update_user_role(
if user is None:
raise NotFoundError("کاربر یافت نشد")
previous_role = user.role
if user.role in ("supervisor", "pm") and payload.role not in ("supervisor", "pm"):
if user.role == "pm" and payload.role != "pm":
await _unassign_projects(db, user.id)
user.role = payload.role
await db.flush()
@@ -234,15 +231,8 @@ async def sync_casdoor_users(
continue
existing = await db.scalar(select(User).where(User.casdoor_id == cid))
if existing is None:
uname = str(cu.get("name") or cu.get("preferred_username") or "") or None
if uname:
by_uname = await db.scalar(select(User).where(User.username == uname))
if by_uname is not None:
by_uname.casdoor_id = cid
continue
db.add(User(
casdoor_id=cid,
username=uname,
first_name=str(
cu.get("firstName")
or cu.get("displayName")
@@ -253,20 +243,16 @@ async def sync_casdoor_users(
phone=str(cu.get("phone") or "") or None,
))
created += 1
elif not existing.username:
uname = str(cu.get("name") or cu.get("preferred_username") or "") or None
if uname:
existing.username = uname
await db.flush()
users = (await db.execute(select(User).order_by(User.created_at))).scalars().all()
supervisor_ids = {u.id for u in users if u.role in ("supervisor", "pm")}
pm_ids = {u.id for u in users if u.role == "pm"}
by_user: dict = {}
if supervisor_ids:
if pm_ids:
projs = (
await db.execute(
select(Project)
.where(Project.pm_user_id.in_(supervisor_ids))
.where(Project.pm_user_id.in_(pm_ids))
.order_by(Project.created_at, Project.id)
)
).scalars().all()
@@ -296,16 +282,11 @@ async def assign_user_project(
user = await db.get(User, user_id)
if user is None:
raise NotFoundError("کاربر یافت نشد")
if user.role not in ("supervisor", "pm"):
raise ValidationError("ابتدا نقش این کاربر را «سرپرست پروژه» کنید", field="role")
if user.role != "pm":
raise ValidationError("ابتدا نقش این کاربر را «مدیر پروژه» کنید", field="role")
if payload.project_id is None:
await _unassign_projects(db, user.id)
if user.username:
await db.execute(
text("DELETE FROM project_assignments WHERE user_id IN (SELECT id FROM pm_users WHERE username = :uname)"),
{"uname": user.username}
)
await db.flush()
return {"data": user_to_dict(user)}
@@ -313,19 +294,5 @@ async def assign_user_project(
if project is None:
raise NotFoundError("پروژه یافت نشد", field="project_id")
project.pm_user_id = user.id
if user.username:
await db.execute(
text("""
INSERT INTO project_assignments (id, user_id, project_id)
SELECT gen_random_uuid(), pu.id, :pid
FROM pm_users pu
WHERE pu.username = :uname
AND NOT EXISTS (
SELECT 1 FROM project_assignments pa
WHERE pa.user_id = pu.id AND pa.project_id = :pid
)
"""),
{"pid": project.id, "uname": user.username}
)
await db.flush()
return {"data": user_to_dict(user, await _user_projects(db, user))}