Private
Public Access
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>
54 lines
2.0 KiB
Python
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])
|