Private
Public Access
Room info panel is now user-resizable (fixing a layout clip at narrow widths), and every user-selection spot (room membership, admin ownership transfer) uses a new searchable UserPicker instead of raw text input or prompt(). Member rows fold role + actions into a single inline dropdown instead of a row of buttons, so the member list stays usable as rooms grow. Room invites (the accept/decline flow) are replaced by adding a user to a room directly -- an admin/owner picks someone and they're a member immediately, with a "you've been added" notification email instead of an invite email. Drops the now-unused room_invites table.
41 lines
1.6 KiB
Python
41 lines
1.6 KiB
Python
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import DateTime, Enum, ForeignKey, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.models.base import Base
|
|
from app.models.invite import InviteStatus
|
|
|
|
DEFAULT_SITE_INVITE_LIFETIME = timedelta(days=7)
|
|
|
|
|
|
def _default_expires_at() -> datetime:
|
|
return datetime.now(timezone.utc) + DEFAULT_SITE_INVITE_LIFETIME
|
|
|
|
|
|
class SiteInvite(Base):
|
|
"""An admin-issued invite for someone with no account yet -- distinct
|
|
from adding an existing user directly to a room."""
|
|
|
|
__tablename__ = "site_invites"
|
|
|
|
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
|
email: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
|
|
invited_by: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), nullable=False)
|
|
# The raw token only ever exists in the invite email link -- like an API
|
|
# token, it's a bearer secret looked up by itself, so it's stored hashed
|
|
# (app.security.hash_token), not in plaintext.
|
|
token_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True, nullable=False)
|
|
status: Mapped[InviteStatus] = mapped_column(
|
|
Enum(InviteStatus, name="invite_status"), default=InviteStatus.pending, nullable=False
|
|
)
|
|
expires_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=_default_expires_at, nullable=False
|
|
)
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), nullable=False
|
|
)
|
|
|
|
inviter = relationship("User")
|