Files
ds-chat/backend/app/models/invite.py
T
ksmithandClaude Sonnet 5 c79e96dd48 Phase 2: private rooms, room roles, and invites (backend only)
Adds room_invites table + migration, owner/admin/member role enforcement
(require_room_role), and endpoints for private room creation, room
management (update/delete/leave/transfer-ownership/change-role/remove-member),
and the invite lifecycle (create/list/accept/decline/revoke). Registration
stays invite-only via the CLI from Phase 1 — this is a separate, room-level
invite system for adding existing users to private rooms.

Frontend is untouched: the UI redesign is happening separately, so this phase
is backend + tests only (35 passing). Verified no regressions in the Phase 1
open-room/WebSocket flow via manual smoke test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 20:22:15 -06:00

54 lines
2.0 KiB
Python

import enum
import uuid
from datetime import datetime, timedelta, timezone
from sqlalchemy import CheckConstraint, DateTime, Enum, ForeignKey, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.models.base import Base
DEFAULT_INVITE_LIFETIME = timedelta(days=7)
def _default_expires_at() -> datetime:
return datetime.now(timezone.utc) + DEFAULT_INVITE_LIFETIME
class InviteStatus(str, enum.Enum):
pending = "pending"
accepted = "accepted"
revoked = "revoked"
class RoomInvite(Base):
__tablename__ = "room_invites"
__table_args__ = (
CheckConstraint(
"target_user_id IS NOT NULL OR target_email IS NOT NULL",
name="room_invites_target_required",
),
)
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
room_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("rooms.id"), index=True, nullable=False)
invited_by: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
token: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
target_user_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("users.id"), index=True)
# Stored per the documented schema but not actionable yet: there's no
# email-delivery mechanism anywhere in the stack. Phase 2 only creates
# invites via target_user_id (existing users, looked up by username).
target_email: Mapped[str | None] = mapped_column(String(255))
expires_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=_default_expires_at, nullable=False
)
status: Mapped[InviteStatus] = mapped_column(
Enum(InviteStatus, name="invite_status"), default=InviteStatus.pending, nullable=False
)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), nullable=False
)
room = relationship("Room")
inviter = relationship("User", foreign_keys=[invited_by])
target_user = relationship("User", foreign_keys=[target_user_id])