Introduces the multi-tenant SaaS foundation alongside the existing client/team/project model (zero-downtime shim period): Backend: - app/models/organization.py — Organization + OrgRole enum (OWNER/ADMIN/MANAGER/MEMBER/VIEWER) - app/models/membership.py — Membership model with MemberDetail for enriched responses - app/services/membership_service.py — upsert/remove/list/has_org_role helpers - app/api/v1/routes_organizations.py — /organizations CRUD + /members sub-resource + /me/memberships - main.py — registers organizations router - migrations: create memberships collection (unique index) + backfill from pm_client_ids/team members Frontend: - types/api.ts — Organization, OrgRole, Membership, OrganizationCreateRequest types; Client marked @deprecated - hooks/useClients.ts — useOrganizations, useOrganization, useOrgMembers, useAddOrgMember, useUpdateOrgMember, useRemoveOrgMember, useMyMemberships - lib/api.ts — listOrganizations, getOrganization, createOrganization, updateOrganization, listOrgMembers, addOrgMember, updateOrgMember, removeOrgMember, getMyMemberships Reads fall back to the clients collection during transition; all writes go to organizations. Existing /clients endpoints and hooks are untouched. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
51 lines
1.3 KiB
Python
51 lines
1.3 KiB
Python
from datetime import datetime
|
|
from enum import Enum
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel
|
|
|
|
|
|
class OrgRole(str, Enum):
|
|
OWNER = "owner"
|
|
ADMIN = "admin"
|
|
MANAGER = "manager"
|
|
MEMBER = "member"
|
|
VIEWER = "viewer"
|
|
|
|
@classmethod
|
|
def _ordering(cls) -> list:
|
|
return [cls.VIEWER, cls.MEMBER, cls.MANAGER, cls.ADMIN, cls.OWNER]
|
|
|
|
def __ge__(self, other: "OrgRole") -> bool:
|
|
return self._ordering().index(self) >= self._ordering().index(other)
|
|
|
|
def __gt__(self, other: "OrgRole") -> bool:
|
|
return self._ordering().index(self) > self._ordering().index(other)
|
|
|
|
def __le__(self, other: "OrgRole") -> bool:
|
|
return self._ordering().index(self) <= self._ordering().index(other)
|
|
|
|
def __lt__(self, other: "OrgRole") -> bool:
|
|
return self._ordering().index(self) < self._ordering().index(other)
|
|
|
|
|
|
class Organization(BaseModel):
|
|
id: Optional[str] = None
|
|
name: str
|
|
slug: str
|
|
is_active: bool = True
|
|
plan: str = "standard"
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
|
|
|
|
class OrganizationCreate(BaseModel):
|
|
name: str
|
|
slug: str
|
|
|
|
|
|
class OrganizationUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
slug: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
plan: Optional[str] = None
|