Ctrl K
ComunidadGuías…

invites.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Invite codes: the hosted beta's registration gate.

An invite code is a random opaque token the operator generates in /admin and
hands to a specific person. Registration (multi mode, when
settings.INVITES_REQUIRED) consumes one with a SINGLE conditional UPDATE
stamped with the new user's id, inside the same transaction as the user
INSERT. There is no hold phase: either the user row and the consumed code
commit together, or (duplicate-email race, code failure) neither does.
Two simultaneous signups with the same code: the first UPDATE wins, the
second matches zero rows and aborts its signup. Race structurally closed.

Codes are not hashed: they are operator-minted, single-use, revocable, and
short-lived in practice; a DB leak is already game over for session secrets.
"""

from __future__ import annotations

import secrets
import settings
from utils import utc_now_str as _now_str


class InviteError(ValueError):
    """Raised when a supplied invite code cannot be used."""


def generate_code() -> str:
    """A new invite code: 12 URL-safe characters, no padding."""
    return secrets.token_urlsafe(9)[:12]


def _normalize(code: str) -> str:
    """Codes are compared case-insensitively and ignore surrounding space."""
    return (code or "").strip().upper()


def create_codes(db, count: int, created_by: int) -> list[str]:
    """Insert ``count`` fresh codes. Returns the plaintext codes."""
    codes: list[str] = []
    for _ in range(max(1, min(count, 50))):
        code = generate_code()
        db.execute(
            "INSERT INTO invite_codes (code, created_by) VALUES (?, ?)",
            (_normalize(code), created_by),
        )
        codes.append(_normalize(code))
    return codes


def validate_and_consume(db, code: str, user_id: int) -> str:
    """Atomically consume an invite code, stamping it with its consumer.

    Called AFTER the user row exists, inside the same transaction. The
    single conditional UPDATE (unused + unrevoked + matching code, rowcount
    1) is the entire validation and the entire claim: there is no hold
    phase, no SELECT-then-UPDATE window, and a rolled-back user INSERT
    rolls the consume back with it. Raises InviteError when the code is
    unknown/used/revoked — the caller must abort the signup.
    """
    normalized = _normalize(code)
    if not normalized:
        raise InviteError("An invite code is required to sign up.")
    result = db.execute(
        """
        UPDATE invite_codes
           SET used_by = ?, used_at = ?
         WHERE code = ? AND used_by IS NULL AND revoked = 0
        """,
        (user_id, _now_str(), normalized),
    )
    if result.rowcount != 1:
        # One message for every failure mode: no code-state enumeration for
        # visitors probing the endpoint.
        raise InviteError("That invite code is not valid.")
    return normalized


def looks_usable(db, code: str) -> bool:
    """Non-consuming pre-check for early form validation UX only.

    NEVER authoritative — the atomic consume in validate_and_consume decides.
    Exists so the register form can render its error banner (HTML) instead of
    a JSON 400 from mid-transaction, without creating any claim on the code.
    """
    normalized = _normalize(code)
    if not normalized:
        return False
    row = db.execute(
        "SELECT 1 FROM invite_codes WHERE code = ? AND used_by IS NULL AND revoked = 0",
        (normalized,),
    ).fetchone()
    return row is not None


def list_codes(db) -> list:
    """All codes, newest first, for the admin table."""
    return db.execute(
        """
        SELECT code, created_by, used_by, used_at, revoked, created_at
          FROM invite_codes
         ORDER BY created_at DESC
        """
    ).fetchall()


def revoke(db, code: str, admin_uid: int) -> bool:
    """Revoke an unused code. Returns False for unknown/already-used codes."""
    result = db.execute(
        """
        UPDATE invite_codes SET revoked = 1
         WHERE code = ? AND used_by IS NULL AND revoked = 0
        """,
        (_normalize(code),),
    )
    if result.rowcount == 1:
        return True
    return False


def invites_required() -> bool:
    """Whether /register demands an invite code (multi mode only)."""
    return settings.MULTI and settings.INVITES_REQUIRED