feat: add governed public self-enrollment links

This commit is contained in:
2026-08-20 13:03:57 +02:00
parent 79cfaa951a
commit 539e3cbb5e
18 changed files with 2481 additions and 13 deletions
+44 -1
View File
@@ -4,7 +4,7 @@ import uuid
from datetime import datetime
from typing import Any
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from govoplan_core.db.base import Base, TimestampMixin
@@ -67,6 +67,40 @@ class SchedulingRequest(Base, TimestampMixin):
cascade="all, delete-orphan",
order_by="SchedulingParticipant.created_at",
)
enrollment_links: Mapped[list["SchedulingPublicEnrollmentLink"]] = relationship(
back_populates="request",
cascade="all, delete-orphan",
order_by="SchedulingPublicEnrollmentLink.created_at",
)
class SchedulingPublicEnrollmentLink(Base, TimestampMixin):
"""Reusable public credential that may create bounded participants."""
__tablename__ = "scheduling_public_enrollment_links"
__table_args__ = (
UniqueConstraint("token_hash", name="uq_scheduling_enrollment_link_token_hash"),
Index("ix_scheduling_enrollment_links_request", "tenant_id", "request_id"),
Index("ix_scheduling_enrollment_links_expiry", "tenant_id", "expires_at"),
)
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
tenant_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
request_id: Mapped[str] = mapped_column(
ForeignKey("scheduling_requests.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
token_hash: Mapped[str] = mapped_column(String(64), nullable=False)
max_enrollments: Mapped[int] = mapped_column(Integer, nullable=False)
expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, index=True)
allow_anonymous: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
allow_authenticated: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_by: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True, index=True)
metadata_: Mapped[dict[str, Any] | None] = mapped_column("metadata", JSON, nullable=True)
request: Mapped[SchedulingRequest] = relationship(back_populates="enrollment_links")
class SchedulingCandidateSlot(Base, TimestampMixin):
@@ -116,6 +150,14 @@ class SchedulingParticipant(Base, TimestampMixin):
status: Mapped[str] = mapped_column(String(40), default="invited", nullable=False, index=True)
poll_invitation_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
participation_gateway: Mapped[str | None] = mapped_column(String(40), nullable=True)
self_enrollment_link_id: Mapped[str | None] = mapped_column(
ForeignKey("scheduling_public_enrollment_links.id", ondelete="SET NULL"),
nullable=True,
index=True,
)
self_enrollment_proof_hash: Mapped[str | None] = mapped_column(String(64), nullable=True)
bound_account_id: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True)
account_bound_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
last_invited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
responded_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
response_comment: Mapped[str | None] = mapped_column(Text, nullable=True)
@@ -150,6 +192,7 @@ __all__ = [
"SchedulingCandidateSlot",
"SchedulingNotification",
"SchedulingParticipant",
"SchedulingPublicEnrollmentLink",
"SchedulingRequest",
"new_uuid",
]