نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی
مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت. تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
"""Repositories for Identity & Access.
|
||||
|
||||
Each repository maps between the ORM rows and the pure domain dataclasses, so
|
||||
the domain layer never sees SQLAlchemy.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.domain.identity.models import (
|
||||
Invite,
|
||||
Membership,
|
||||
MembershipStatus,
|
||||
Organization,
|
||||
Role,
|
||||
User,
|
||||
)
|
||||
from app.infrastructure.db.models import (
|
||||
InviteRow,
|
||||
MembershipRow,
|
||||
OrganizationRow,
|
||||
UserRow,
|
||||
)
|
||||
|
||||
|
||||
def _user_to_domain(row: UserRow) -> User:
|
||||
return User(
|
||||
id=row.id,
|
||||
email=row.email,
|
||||
password_hash=row.password_hash,
|
||||
name=row.name,
|
||||
avatar_url=row.avatar_url,
|
||||
is_platform_owner=row.is_platform_owner,
|
||||
is_active=row.is_active,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _org_to_domain(row: OrganizationRow) -> Organization:
|
||||
return Organization(
|
||||
id=row.id, name=row.name, slug=row.slug,
|
||||
owner_user_id=row.owner_user_id, created_at=row.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _membership_to_domain(row: MembershipRow) -> Membership:
|
||||
return Membership(
|
||||
id=row.id,
|
||||
organization_id=row.organization_id,
|
||||
user_id=row.user_id,
|
||||
role=Role(row.role),
|
||||
status=MembershipStatus(row.status),
|
||||
created_at=row.created_at,
|
||||
)
|
||||
|
||||
|
||||
def _invite_to_domain(row: InviteRow) -> Invite:
|
||||
return Invite(
|
||||
id=row.id,
|
||||
organization_id=row.organization_id,
|
||||
email=row.email,
|
||||
role=Role(row.role),
|
||||
token_hash=row.token_hash,
|
||||
invited_by_user_id=row.invited_by_user_id,
|
||||
expires_at=row.expires_at,
|
||||
accepted_at=row.accepted_at,
|
||||
created_at=row.created_at,
|
||||
)
|
||||
|
||||
|
||||
class UserRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session = session
|
||||
|
||||
async def get(self, user_id: str) -> User | None:
|
||||
row = await self.session.get(UserRow, user_id)
|
||||
return _user_to_domain(row) if row else None
|
||||
|
||||
async def get_by_email(self, email: str) -> User | None:
|
||||
stmt = select(UserRow).where(func.lower(UserRow.email) == email.strip().lower())
|
||||
row = (await self.session.execute(stmt)).scalar_one_or_none()
|
||||
return _user_to_domain(row) if row else None
|
||||
|
||||
async def create(self, user: User) -> User:
|
||||
row = UserRow(
|
||||
id=user.id,
|
||||
email=user.normalized_email(),
|
||||
password_hash=user.password_hash,
|
||||
name=user.name,
|
||||
avatar_url=user.avatar_url,
|
||||
is_platform_owner=user.is_platform_owner,
|
||||
is_active=user.is_active,
|
||||
created_at=user.created_at,
|
||||
updated_at=user.updated_at,
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.flush()
|
||||
return _user_to_domain(row)
|
||||
|
||||
async def update_password(self, user_id: str, password_hash: str) -> None:
|
||||
row = await self.session.get(UserRow, user_id)
|
||||
if row:
|
||||
row.password_hash = password_hash
|
||||
await self.session.flush()
|
||||
|
||||
async def count(self) -> int:
|
||||
return int((await self.session.execute(select(func.count(UserRow.id)))).scalar_one())
|
||||
|
||||
|
||||
class OrganizationRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session = session
|
||||
|
||||
async def get(self, org_id: str) -> Organization | None:
|
||||
row = await self.session.get(OrganizationRow, org_id)
|
||||
return _org_to_domain(row) if row else None
|
||||
|
||||
async def get_by_slug(self, slug: str) -> Organization | None:
|
||||
stmt = select(OrganizationRow).where(OrganizationRow.slug == slug)
|
||||
row = (await self.session.execute(stmt)).scalar_one_or_none()
|
||||
return _org_to_domain(row) if row else None
|
||||
|
||||
async def list_all_slugs(self) -> set[str]:
|
||||
rows = (await self.session.execute(select(OrganizationRow.slug))).scalars().all()
|
||||
return set(rows)
|
||||
|
||||
async def create(self, org: Organization) -> Organization:
|
||||
row = OrganizationRow(
|
||||
id=org.id, name=org.name, slug=org.slug,
|
||||
owner_user_id=org.owner_user_id, created_at=org.created_at,
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.flush()
|
||||
return _org_to_domain(row)
|
||||
|
||||
async def list_for_user(self, user_id: str) -> list[tuple[Organization, Role]]:
|
||||
stmt = (
|
||||
select(OrganizationRow, MembershipRow.role)
|
||||
.join(MembershipRow, MembershipRow.organization_id == OrganizationRow.id)
|
||||
.where(MembershipRow.user_id == user_id, MembershipRow.status == "active")
|
||||
.order_by(OrganizationRow.created_at)
|
||||
)
|
||||
rows = (await self.session.execute(stmt)).all()
|
||||
return [(_org_to_domain(o), Role(r)) for o, r in rows]
|
||||
|
||||
async def list_all(self) -> list[Organization]:
|
||||
"""Every organization. Platform-owner console only — never expose this
|
||||
through a tenant-scoped endpoint."""
|
||||
stmt = select(OrganizationRow).order_by(OrganizationRow.created_at)
|
||||
return [_org_to_domain(r) for r in (await self.session.execute(stmt)).scalars()]
|
||||
|
||||
async def delete(self, org_id: str) -> None:
|
||||
row = await self.session.get(OrganizationRow, org_id)
|
||||
if row:
|
||||
await self.session.delete(row)
|
||||
await self.session.flush()
|
||||
|
||||
async def count(self) -> int:
|
||||
return int(
|
||||
(await self.session.execute(select(func.count(OrganizationRow.id)))).scalar_one()
|
||||
)
|
||||
|
||||
|
||||
class MembershipRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session = session
|
||||
|
||||
async def get(self, org_id: str, user_id: str) -> Membership | None:
|
||||
stmt = select(MembershipRow).where(
|
||||
MembershipRow.organization_id == org_id, MembershipRow.user_id == user_id
|
||||
)
|
||||
row = (await self.session.execute(stmt)).scalar_one_or_none()
|
||||
return _membership_to_domain(row) if row else None
|
||||
|
||||
async def get_by_id(self, membership_id: str) -> Membership | None:
|
||||
row = await self.session.get(MembershipRow, membership_id)
|
||||
return _membership_to_domain(row) if row else None
|
||||
|
||||
async def list_for_org(self, org_id: str) -> list[Membership]:
|
||||
stmt = (
|
||||
select(MembershipRow)
|
||||
.where(MembershipRow.organization_id == org_id)
|
||||
.order_by(MembershipRow.created_at)
|
||||
)
|
||||
return [_membership_to_domain(r) for r in (await self.session.execute(stmt)).scalars()]
|
||||
|
||||
async def add(self, membership: Membership) -> Membership:
|
||||
row = MembershipRow(
|
||||
id=membership.id,
|
||||
organization_id=membership.organization_id,
|
||||
user_id=membership.user_id,
|
||||
role=membership.role.value,
|
||||
status=membership.status.value,
|
||||
created_at=membership.created_at,
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.flush()
|
||||
return _membership_to_domain(row)
|
||||
|
||||
async def set_role(self, membership_id: str, role: Role) -> None:
|
||||
row = await self.session.get(MembershipRow, membership_id)
|
||||
if row:
|
||||
row.role = role.value
|
||||
await self.session.flush()
|
||||
|
||||
async def set_status(self, membership_id: str, status: MembershipStatus) -> None:
|
||||
row = await self.session.get(MembershipRow, membership_id)
|
||||
if row:
|
||||
row.status = status.value
|
||||
await self.session.flush()
|
||||
|
||||
async def remove(self, membership_id: str) -> None:
|
||||
row = await self.session.get(MembershipRow, membership_id)
|
||||
if row:
|
||||
await self.session.delete(row)
|
||||
await self.session.flush()
|
||||
|
||||
async def count_owners(self, org_id: str) -> int:
|
||||
stmt = select(func.count(MembershipRow.id)).where(
|
||||
MembershipRow.organization_id == org_id,
|
||||
MembershipRow.role == Role.OWNER.value,
|
||||
MembershipRow.status == MembershipStatus.ACTIVE.value,
|
||||
)
|
||||
return int((await self.session.execute(stmt)).scalar_one())
|
||||
|
||||
|
||||
class InviteRepository:
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
self.session = session
|
||||
|
||||
async def get_by_token_hash(self, token_hash: str) -> Invite | None:
|
||||
stmt = select(InviteRow).where(InviteRow.token_hash == token_hash)
|
||||
row = (await self.session.execute(stmt)).scalar_one_or_none()
|
||||
return _invite_to_domain(row) if row else None
|
||||
|
||||
async def list_pending(self, org_id: str) -> list[Invite]:
|
||||
stmt = (
|
||||
select(InviteRow)
|
||||
.where(InviteRow.organization_id == org_id, InviteRow.accepted_at.is_(None))
|
||||
.order_by(InviteRow.created_at.desc())
|
||||
)
|
||||
return [_invite_to_domain(r) for r in (await self.session.execute(stmt)).scalars()]
|
||||
|
||||
async def create(self, invite: Invite) -> Invite:
|
||||
row = InviteRow(
|
||||
id=invite.id,
|
||||
organization_id=invite.organization_id,
|
||||
email=invite.email.strip().lower(),
|
||||
role=invite.role.value,
|
||||
token_hash=invite.token_hash,
|
||||
invited_by_user_id=invite.invited_by_user_id,
|
||||
expires_at=invite.expires_at,
|
||||
accepted_at=invite.accepted_at,
|
||||
created_at=invite.created_at,
|
||||
)
|
||||
self.session.add(row)
|
||||
await self.session.flush()
|
||||
return _invite_to_domain(row)
|
||||
|
||||
async def mark_accepted(self, invite_id: str, when: datetime) -> None:
|
||||
row = await self.session.get(InviteRow, invite_id)
|
||||
if row:
|
||||
row.accepted_at = when
|
||||
await self.session.flush()
|
||||
Reference in New Issue
Block a user