- Invitations: each user can invite two friends by email (sign-up works through the link even while registration is closed); admin-editable template, subject and sender. - Invitation sender defaults to the system-mail From address, which the SMTP account is allowed to send as (was postmaster@APP_DOMAIN, refused with 553 when the two differ). - Sign-in, sign-up and forgot-password fields show the fixed @domain after the typed name; the registration status reports it. - System mail gets a Date header; the invitation's plain-text part keeps its paragraphs and line breaks. - SMTP EHLO uses SMTP_HELO_NAME / APP_DOMAIN, not the container name. - DNS recommendations for the mail domains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.0 KiB
Python
64 lines
2.0 KiB
Python
"""invitations: users invite friends, even while sign-up is closed
|
|
|
|
An administrator switch lets every user send a few single-use invitation links
|
|
by email; a link opens sign-up for the person it was sent to even when public
|
|
registration is off. The email is built from a template the administrator
|
|
uploads (or the built-in one) and sent from the instance's own address.
|
|
|
|
Revision ID: 0027
|
|
Revises: 0026
|
|
Create Date: 2026-10-14
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Sequence
|
|
|
|
from alembic import op
|
|
|
|
revision: str = "0027"
|
|
down_revision: str | None = "0026"
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.execute(
|
|
"""
|
|
ALTER TABLE app_settings
|
|
ADD COLUMN invitations_enabled boolean NOT NULL DEFAULT false,
|
|
ADD COLUMN invite_from text NOT NULL DEFAULT '',
|
|
ADD COLUMN invite_subject text NOT NULL DEFAULT '',
|
|
ADD COLUMN invite_template text NOT NULL DEFAULT ''
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TABLE invitations (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
inviter_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash text NOT NULL UNIQUE,
|
|
invitee_name text NOT NULL,
|
|
invitee_email text NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz NOT NULL,
|
|
used_at timestamptz,
|
|
used_by uuid REFERENCES users(id) ON DELETE SET NULL
|
|
)
|
|
"""
|
|
)
|
|
op.execute("CREATE INDEX ix_invitations_inviter ON invitations (inviter_id)")
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.execute("DROP TABLE IF EXISTS invitations")
|
|
op.execute(
|
|
"""
|
|
ALTER TABLE app_settings
|
|
DROP COLUMN IF EXISTS invitations_enabled,
|
|
DROP COLUMN IF EXISTS invite_from,
|
|
DROP COLUMN IF EXISTS invite_subject,
|
|
DROP COLUMN IF EXISTS invite_template
|
|
"""
|
|
)
|