Squashed 'pm/' content from commit 91f3271
git-subtree-dir: pm git-subtree-split: 91f3271d866fa8ee471cfb6793e334b83e6d2b6e
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""Development-only demo accounts for the role switcher on the dashboard.
|
||||
|
||||
While the two roles are being built, the «امروز» dashboard shows a role picker
|
||||
so nobody has to juggle logins. This module creates those demo users and gives
|
||||
the demo supervisors a project scope, so the picker is immediately useful —
|
||||
the enforcement itself is the real thing (see app/authz.py).
|
||||
|
||||
Everything here is inert unless `PM_DEV_ROLE_SWITCH` is enabled; the endpoints
|
||||
in routers/auth.py return 404 in that case. For production set the flag to
|
||||
false and manage users through `PUT /users/{id}/projects`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.authz import Role, dev_switch_enabled
|
||||
from app.config import settings
|
||||
from app.models import PMUser, Project, ProjectAssignment
|
||||
from app.security import hash_password
|
||||
|
||||
logger = logging.getLogger("pm.dev")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DemoAccount:
|
||||
username: str
|
||||
password: str
|
||||
display_name: str
|
||||
role: Role
|
||||
share: tuple[int, int] | None = None
|
||||
|
||||
|
||||
def demo_accounts() -> list[DemoAccount]:
|
||||
total = len(settings.dev_demo_usernames())
|
||||
return [
|
||||
DemoAccount(settings.ADMIN_USER, settings.ADMIN_PASSWORD, "مدیر سیستم", Role.project_manager),
|
||||
DemoAccount(
|
||||
settings.DEV_SUPERVISOR_USER,
|
||||
settings.DEV_SUPERVISOR_PASSWORD,
|
||||
"سرپرست پروژه الف",
|
||||
Role.project_supervisor,
|
||||
share=(1, 2),
|
||||
),
|
||||
DemoAccount(
|
||||
settings.DEV_SUPERVISOR2_USER,
|
||||
settings.DEV_SUPERVISOR2_PASSWORD,
|
||||
"سرپرست پروژه ب",
|
||||
Role.project_supervisor,
|
||||
share=(2, 2),
|
||||
),
|
||||
][:total]
|
||||
|
||||
|
||||
def _share_of(items: list, share: tuple[int, int]) -> list:
|
||||
"""Deterministic, non-overlapping slice: part N of M (1-based)."""
|
||||
part, of = share
|
||||
size = -(-len(items) // of)
|
||||
start = (part - 1) * size
|
||||
return items[start : start + size]
|
||||
|
||||
|
||||
def demo_account(username: str) -> DemoAccount | None:
|
||||
for account in demo_accounts():
|
||||
if account.username == username:
|
||||
return account
|
||||
return None
|
||||
|
||||
|
||||
async def ensure_demo_accounts(db: AsyncSession) -> None:
|
||||
if not dev_switch_enabled():
|
||||
# در پروداکشن، حسابهای نمایشی توسعه نباید فعال باشند
|
||||
demo_names = [settings.DEV_SUPERVISOR_USER, settings.DEV_SUPERVISOR2_USER]
|
||||
demo_users = (
|
||||
await db.execute(select(PMUser).where(PMUser.username.in_(demo_names)))
|
||||
).scalars().all()
|
||||
for u in demo_users:
|
||||
await db.execute(delete(ProjectAssignment).where(ProjectAssignment.user_id == u.id))
|
||||
await db.delete(u)
|
||||
logger.info("cleaned up demo account %s for production", u.username)
|
||||
admin_u = await db.scalar(select(PMUser).where(PMUser.username == settings.ADMIN_USER))
|
||||
if admin_u is None:
|
||||
db.add(
|
||||
PMUser(
|
||||
username=settings.ADMIN_USER,
|
||||
password_hash=hash_password(settings.ADMIN_PASSWORD),
|
||||
display_name="مدیر سیستم",
|
||||
role=Role.project_manager.value,
|
||||
)
|
||||
)
|
||||
logger.info("seeded admin account in production mode")
|
||||
else:
|
||||
# همیشه رمز عبور را از تنظیمات بهروز کن تا لاگین همیشه کار کند
|
||||
admin_u.password_hash = hash_password(settings.ADMIN_PASSWORD)
|
||||
admin_u.role = Role.project_manager.value
|
||||
logger.info("refreshed admin password hash in production mode")
|
||||
await db.commit()
|
||||
return
|
||||
for account in demo_accounts():
|
||||
user = await db.scalar(select(PMUser).where(PMUser.username == account.username))
|
||||
if user is None:
|
||||
db.add(
|
||||
PMUser(
|
||||
username=account.username,
|
||||
password_hash=hash_password(account.password),
|
||||
display_name=account.display_name,
|
||||
role=account.role.value,
|
||||
)
|
||||
)
|
||||
logger.info("seeded %s account %s", account.role.value, account.username)
|
||||
else:
|
||||
user.role = account.role.value
|
||||
user.display_name = account.display_name
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def ensure_demo_assignments(db: AsyncSession) -> None:
|
||||
if not dev_switch_enabled():
|
||||
return
|
||||
|
||||
native_ids = [p for p in (await db.execute(select(Project.id).order_by(Project.name))).scalars().all()]
|
||||
if not native_ids:
|
||||
return
|
||||
|
||||
for account in demo_accounts():
|
||||
if account.share is None:
|
||||
continue
|
||||
user = await db.scalar(select(PMUser).where(PMUser.username == account.username))
|
||||
if user is None:
|
||||
continue
|
||||
existing = await db.scalar(
|
||||
select(func.count()).where(ProjectAssignment.user_id == user.id)
|
||||
)
|
||||
if existing:
|
||||
continue
|
||||
mine = _share_of(native_ids, account.share)
|
||||
for project_uuid in mine:
|
||||
db.add(ProjectAssignment(user_id=user.id, project_id=project_uuid))
|
||||
logger.info(
|
||||
"assigned %d demo projects to %s",
|
||||
len(mine),
|
||||
account.username,
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def clear_assignments(db: AsyncSession, user_id) -> None:
|
||||
await db.execute(delete(ProjectAssignment).where(ProjectAssignment.user_id == user_id))
|
||||
Reference in New Issue
Block a user