Silos/backend/app/services/tenant_service.py
AnuragJangid18 bf5cd44dc1 feat: implement workspace management and member invitations
- Added tenant API for managing workspaces, including listing, creating, and inviting members.
- Introduced new models for organizations, workspaces, and workspace memberships in the backend.
- Created frontend components for workspace administration, allowing users to manage workspaces and invite members.
- Updated document listing to support workspace-specific filtering.
- Implemented account settings page for user profile management and password changes.
- Added end-to-end tests for workspace administration and dashboard functionalities.
2026-10-23 01:19:11 +05:30

159 lines
6.1 KiB
Python

import re
import secrets
from datetime import UTC, datetime, timedelta
from sqlalchemy import or_
from sqlalchemy.orm import Session
from app.models.tenant import Organization, TenantAuditEvent, Workspace, WorkspaceInvitation, WorkspaceMembership
from app.models.user import User
def slugify(value: str) -> str:
return re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")[:170] or "workspace"
def get_membership(db: Session, workspace_id: int, user_id: int) -> WorkspaceMembership | None:
return db.query(WorkspaceMembership).filter_by(workspace_id=workspace_id, user_id=user_id).first()
def can_manage(role: str | None) -> bool:
return role in {"owner", "admin"}
def audit(db: Session, organization_id: int, workspace_id: int | None, actor_id: int, action: str, target: str) -> None:
db.add(TenantAuditEvent(
organization_id=organization_id,
workspace_id=workspace_id,
actor_id=actor_id,
action=action,
target=target,
))
def create_workspace(db: Session, owner: User, data) -> Workspace:
org_slug = slugify(data.organization_name)
if db.query(Organization).filter_by(slug=org_slug).first():
org_slug = f"{org_slug}-{secrets.token_hex(3)}"
organization = Organization(name=data.organization_name.strip(), slug=org_slug, owner_id=owner.id)
db.add(organization)
db.flush()
workspace_slug = slugify(data.workspace_name)
workspace = Workspace(
organization_id=organization.id,
name=data.workspace_name.strip(),
slug=workspace_slug,
)
db.add(workspace)
db.flush()
db.add(WorkspaceMembership(workspace_id=workspace.id, user_id=owner.id, role="owner"))
audit(db, organization.id, workspace.id, owner.id, "workspace.created", workspace.name)
db.commit()
db.refresh(workspace)
return workspace
def list_workspaces(db: Session, user_id: int):
return (
db.query(Workspace, Organization, WorkspaceMembership.role)
.join(Organization, Organization.id == Workspace.organization_id)
.join(WorkspaceMembership, WorkspaceMembership.workspace_id == Workspace.id)
.filter(WorkspaceMembership.user_id == user_id)
.order_by(Workspace.created_at.asc())
.all()
)
def get_workspace(db: Session, workspace_id: int, user_id: int):
return (
db.query(Workspace, Organization, WorkspaceMembership.role)
.join(Organization, Organization.id == Workspace.organization_id)
.join(WorkspaceMembership, WorkspaceMembership.workspace_id == Workspace.id)
.filter(Workspace.id == workspace_id, WorkspaceMembership.user_id == user_id)
.first()
)
def list_members(db: Session, workspace_id: int):
return (
db.query(User, WorkspaceMembership)
.join(WorkspaceMembership, WorkspaceMembership.user_id == User.id)
.filter(WorkspaceMembership.workspace_id == workspace_id)
.order_by(User.full_name.asc())
.all()
)
def invite_member(db: Session, workspace_id: int, inviter_id: int, email: str, role: str):
token = secrets.token_urlsafe(32)
invitation = WorkspaceInvitation(
workspace_id=workspace_id,
inviter_id=inviter_id,
email=email.lower(),
role=role,
token=token,
expires_at=datetime.now(UTC) + timedelta(days=7),
)
db.add(invitation)
workspace = db.query(Workspace).filter_by(id=workspace_id).first()
if workspace:
audit(db, workspace.organization_id, workspace_id, inviter_id, "invitation.created", email.lower())
db.commit()
db.refresh(invitation)
return invitation
def revoke_invitation(db: Session, workspace_id: int, invitation_id: int, actor_id: int) -> bool:
invitation = db.query(WorkspaceInvitation).filter_by(id=invitation_id, workspace_id=workspace_id, accepted=False).first()
if invitation is None:
return False
invitation.expires_at = datetime.now(UTC)
workspace = db.query(Workspace).filter_by(id=workspace_id).first()
if workspace:
audit(db, workspace.organization_id, workspace_id, actor_id, "invitation.revoked", invitation.email)
db.commit()
return True
def transfer_ownership(db: Session, workspace_id: int, current_owner_id: int, next_owner_id: int) -> bool:
current = get_membership(db, workspace_id, current_owner_id)
target = get_membership(db, workspace_id, next_owner_id)
workspace = db.query(Workspace).filter_by(id=workspace_id).first()
if not current or current.role != "owner" or not target or not workspace:
return False
current.role = "admin"
target.role = "owner"
organization = db.query(Organization).filter_by(id=workspace.organization_id).first()
if organization and organization.owner_id == current_owner_id:
organization.owner_id = next_owner_id
audit(db, workspace.organization_id, workspace_id, current_owner_id, "workspace.ownership_transferred", str(next_owner_id))
db.commit()
return True
def list_audit_events(db: Session, workspace_id: int, limit: int = 100):
return db.query(TenantAuditEvent).filter_by(workspace_id=workspace_id).order_by(TenantAuditEvent.created_at.desc()).limit(limit).all()
def accept_invitation(db: Session, token: str, user: User) -> WorkspaceMembership | None:
invitation = db.query(WorkspaceInvitation).filter_by(token=token, accepted=False).first()
if invitation is None or invitation.expires_at < datetime.now(UTC) or invitation.email != user.email.lower():
return None
membership = get_membership(db, invitation.workspace_id, user.id)
if membership is None:
membership = WorkspaceMembership(workspace_id=invitation.workspace_id, user_id=user.id, role=invitation.role)
db.add(membership)
invitation.accepted = True
db.commit()
db.refresh(membership)
return membership
def update_member_role(db: Session, workspace_id: int, target_id: int, role: str) -> WorkspaceMembership | None:
membership = get_membership(db, workspace_id, target_id)
if membership is None or membership.role == "owner":
return None
membership.role = role
db.commit()
db.refresh(membership)
return membership