Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions backend/alembic/versions/add_wecom_org_sync_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Add provider-aware org sync fields.

Revision ID: add_wecom_org_sync_fields
Revises: be48e94fa052
"""

from alembic import op

revision = "add_wecom_org_sync_fields"
down_revision = "be48e94fa052"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.execute(
"""
ALTER TABLE org_departments
ADD COLUMN IF NOT EXISTS wecom_id VARCHAR(100)
"""
)
op.execute(
"""
ALTER TABLE org_departments
ADD COLUMN IF NOT EXISTS sync_provider VARCHAR(20) NOT NULL DEFAULT 'feishu'
"""
)
op.execute(
"""
ALTER TABLE org_members
ADD COLUMN IF NOT EXISTS wecom_user_id VARCHAR(100)
"""
)
op.execute(
"""
ALTER TABLE org_members
ADD COLUMN IF NOT EXISTS sync_provider VARCHAR(20) NOT NULL DEFAULT 'feishu'
"""
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_org_departments_wecom_id ON org_departments(wecom_id)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_org_departments_sync_provider ON org_departments(sync_provider)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_org_members_wecom_user_id ON org_members(wecom_user_id)"
)
op.execute(
"CREATE INDEX IF NOT EXISTS ix_org_members_sync_provider ON org_members(sync_provider)"
)


def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_org_members_sync_provider")
op.execute("DROP INDEX IF EXISTS ix_org_members_wecom_user_id")
op.execute("DROP INDEX IF EXISTS ix_org_departments_sync_provider")
op.execute("DROP INDEX IF EXISTS ix_org_departments_wecom_id")
op.execute("ALTER TABLE org_members DROP COLUMN IF EXISTS sync_provider")
op.execute("ALTER TABLE org_members DROP COLUMN IF EXISTS wecom_user_id")
op.execute("ALTER TABLE org_departments DROP COLUMN IF EXISTS sync_provider")
op.execute("ALTER TABLE org_departments DROP COLUMN IF EXISTS wecom_id")
32 changes: 29 additions & 3 deletions backend/app/api/enterprise.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,14 @@ async def get_system_setting(
db: AsyncSession = Depends(get_db),
):
"""Get a system setting by key."""
if key == "org_sync":
if current_user.role not in ("platform_admin", "org_admin"):
raise HTTPException(status_code=403, detail="Admin access required")
from app.services.org_sync_service import org_sync_service

value = await org_sync_service.get_public_config(db)
return {"key": key, "value": value}

result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
setting = result.scalar_one_or_none()
if not setting:
Expand All @@ -502,6 +510,12 @@ async def update_system_setting(
# Platform-level settings (e.g. PUBLIC_BASE_URL) require platform_admin
if key == "platform" and current_user.role != "platform_admin":
raise HTTPException(status_code=403, detail="Only platform admin can modify platform settings")
if key == "org_sync":
from app.services.org_sync_service import org_sync_service

value = await org_sync_service.save_config(db, data.value)
return {"key": key, "value": value}

result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
setting = result.scalar_one_or_none()
if setting:
Expand All @@ -525,15 +539,20 @@ async def list_org_departments(
db: AsyncSession = Depends(get_db),
):
"""List all departments, optionally filtered by tenant."""
query = select(OrgDepartment)
from app.services.org_sync_service import org_sync_service

provider, _, _ = await org_sync_service.get_active_provider(db)
query = select(OrgDepartment).where(OrgDepartment.sync_provider == provider)
if tenant_id:
query = query.where(OrgDepartment.tenant_id == uuid.UUID(tenant_id))
result = await db.execute(query.order_by(OrgDepartment.name))
depts = result.scalars().all()
return [
{
"id": str(d.id),
"provider": d.sync_provider,
"feishu_id": d.feishu_id,
"wecom_id": d.wecom_id,
"name": d.name,
"parent_id": str(d.parent_id) if d.parent_id else None,
"path": d.path,
Expand All @@ -554,7 +573,13 @@ async def list_org_members(
db: AsyncSession = Depends(get_db),
):
"""List org members, optionally filtered by department, search, or tenant."""
query = select(OrgMember).where(OrgMember.status == "active")
from app.services.org_sync_service import org_sync_service

provider, _, _ = await org_sync_service.get_active_provider(db)
query = select(OrgMember).where(
OrgMember.status == "active",
OrgMember.sync_provider == provider,
)
if tenant_id:
query = query.where(OrgMember.tenant_id == uuid.UUID(tenant_id))
if department_id:
Expand All @@ -573,6 +598,7 @@ async def list_org_members(
return [
{
"id": str(m.id),
"provider": m.sync_provider,
"name": m.name,
"email": m.email,
"title": m.title,
Expand All @@ -587,7 +613,7 @@ async def list_org_members(
async def trigger_org_sync(
current_user: User = Depends(get_current_admin),
):
"""Manually trigger org structure sync from Feishu."""
"""Manually trigger org structure sync from the active provider."""
from app.services.org_sync_service import org_sync_service
result = await org_sync_service.full_sync()
return result
Expand Down
11 changes: 8 additions & 3 deletions backend/app/models/org.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Organization structure models — departments and members synced from Feishu."""
"""Organization structure models cached from external directory providers."""

import uuid
from datetime import datetime
Expand All @@ -8,15 +8,18 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import Base
from app.models.tenant import Tenant # noqa: F401


class OrgDepartment(Base):
"""Department from Feishu org structure."""
"""Department synced from an external directory provider."""

__tablename__ = "org_departments"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
feishu_id: Mapped[str | None] = mapped_column(String(100), unique=True)
wecom_id: Mapped[str | None] = mapped_column(String(100), index=True)
sync_provider: Mapped[str] = mapped_column(String(20), default="feishu", server_default="feishu", nullable=False, index=True)
name: Mapped[str] = mapped_column(String(200), nullable=False)
parent_id: Mapped[uuid.UUID | None] = mapped_column(UUID(as_uuid=True), ForeignKey("org_departments.id"))
path: Mapped[str] = mapped_column(String(500), default="")
Expand All @@ -28,13 +31,15 @@ class OrgDepartment(Base):


class OrgMember(Base):
"""Person from Feishu org structure."""
"""Person synced from an external directory provider."""

__tablename__ = "org_members"

id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
feishu_open_id: Mapped[str | None] = mapped_column(String(100), unique=True)
feishu_user_id: Mapped[str | None] = mapped_column(String(100))
wecom_user_id: Mapped[str | None] = mapped_column(String(100), index=True)
sync_provider: Mapped[str] = mapped_column(String(20), default="feishu", server_default="feishu", nullable=False, index=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
name_translit_full: Mapped[str | None] = mapped_column(String(255), index=True)
name_translit_initial: Mapped[str | None] = mapped_column(String(50), index=True)
Expand Down
Loading