feat(invites): add revoked_at + email_sent_at to account_invites
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
"""account_invites add revoked_at and email_sent_at
|
||||||
|
|
||||||
|
Revision ID: 2aa73d3231c2
|
||||||
|
Revises: e1af7ab57ceb
|
||||||
|
Create Date: 2026-05-06 07:28:28.514384
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2aa73d3231c2'
|
||||||
|
down_revision: Union[str, None] = 'e1af7ab57ceb'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column("account_invites", sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.add_column("account_invites", sa.Column("email_sent_at", sa.DateTime(timezone=True), nullable=True))
|
||||||
|
op.create_index("ix_account_invites_revoked_at", "account_invites", ["revoked_at"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index("ix_account_invites_revoked_at", table_name="account_invites")
|
||||||
|
op.drop_column("account_invites", "email_sent_at")
|
||||||
|
op.drop_column("account_invites", "revoked_at")
|
||||||
@@ -27,6 +27,8 @@ class AccountInvite(Base):
|
|||||||
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
expires_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||||
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
used_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
revoked_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
email_sent_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
account: Mapped["Account"] = relationship("Account")
|
account: Mapped["Account"] = relationship("Account")
|
||||||
@@ -37,6 +39,10 @@ class AccountInvite(Base):
|
|||||||
def is_used(self) -> bool:
|
def is_used(self) -> bool:
|
||||||
return self.accepted_by_id is not None
|
return self.accepted_by_id is not None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_revoked(self) -> bool:
|
||||||
|
return self.revoked_at is not None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
if self.expires_at is None:
|
if self.expires_at is None:
|
||||||
@@ -45,4 +51,4 @@ class AccountInvite(Base):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_valid(self) -> bool:
|
def is_valid(self) -> bool:
|
||||||
return not self.is_used and not self.is_expired
|
return not self.is_used and not self.is_expired and not self.is_revoked
|
||||||
|
|||||||
27
backend/tests/test_account_invite_model.py
Normal file
27
backend/tests/test_account_invite_model.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
from app.models.account_invite import AccountInvite
|
||||||
|
|
||||||
|
|
||||||
|
def make_invite(**kwargs):
|
||||||
|
return AccountInvite(
|
||||||
|
account_id=kwargs.get("account_id", "00000000-0000-0000-0000-000000000001"),
|
||||||
|
invited_by_id=kwargs.get("invited_by_id", "00000000-0000-0000-0000-000000000002"),
|
||||||
|
email=kwargs.get("email", "x@y.com"),
|
||||||
|
code=kwargs.get("code", "ABCD1234"),
|
||||||
|
role=kwargs.get("role", "engineer"),
|
||||||
|
accepted_by_id=kwargs.get("accepted_by_id"),
|
||||||
|
expires_at=kwargs.get("expires_at"),
|
||||||
|
revoked_at=kwargs.get("revoked_at"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_revoked_is_invalid():
|
||||||
|
invite = make_invite(revoked_at=datetime.now(timezone.utc))
|
||||||
|
assert invite.is_revoked is True
|
||||||
|
assert invite.is_valid is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_invite_unrevoked_unexpired_unused_is_valid():
|
||||||
|
invite = make_invite(expires_at=datetime.now(timezone.utc) + timedelta(days=7))
|
||||||
|
assert invite.is_valid is True
|
||||||
Reference in New Issue
Block a user