feat(slice-4): honest delivery failure — send-before-commit + 502 delivery_failed (INV-9; closes #7)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from .errors import (
|
||||
CodeExhausted,
|
||||
CodeExpired,
|
||||
CodeMismatch,
|
||||
DeliveryFailed,
|
||||
InvalidEmail,
|
||||
ResendCooldown,
|
||||
)
|
||||
@@ -25,6 +26,7 @@ __all__ = [
|
||||
"CodeMismatch",
|
||||
"CodeExpired",
|
||||
"CodeExhausted",
|
||||
"DeliveryFailed",
|
||||
"request_code",
|
||||
"verify",
|
||||
"get_account",
|
||||
|
||||
@@ -36,3 +36,7 @@ class CodeExpired(AccountsError):
|
||||
|
||||
class CodeExhausted(AccountsError):
|
||||
"""The code's attempt budget is spent; it is invalidated (INV-3 → §6.4 400 code_exhausted)."""
|
||||
|
||||
|
||||
class DeliveryFailed(AccountsError):
|
||||
"""The relay refused the code email; nothing was committed (INV-9 → §6.4 502 delivery_failed)."""
|
||||
|
||||
@@ -18,9 +18,16 @@ from datetime import datetime, timedelta, timezone
|
||||
import psycopg
|
||||
|
||||
from app.platform import config
|
||||
from app.platform.mailer import Mailer
|
||||
from app.platform.mailer import Mailer, MailerError
|
||||
|
||||
from .errors import CodeExhausted, CodeExpired, CodeMismatch, InvalidEmail, ResendCooldown
|
||||
from .errors import (
|
||||
CodeExhausted,
|
||||
CodeExpired,
|
||||
CodeMismatch,
|
||||
DeliveryFailed,
|
||||
InvalidEmail,
|
||||
ResendCooldown,
|
||||
)
|
||||
from .models import Account
|
||||
|
||||
# INV-3 constants — the one-time-code policy.
|
||||
@@ -82,17 +89,23 @@ def request_code(conn: psycopg.Connection, mailer: Mailer, email: str) -> None:
|
||||
"INSERT INTO auth_code (email, code_hash, expires_at) VALUES (%s, %s, %s)",
|
||||
(email, _hash_code(code), now + CODE_TTL),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
mailer.send(
|
||||
to=email,
|
||||
subject=f"Your ecomm code: {code}",
|
||||
body=(
|
||||
f"Your ecomm one-time code is {code}.\n"
|
||||
f"It is valid for {int(CODE_TTL.total_seconds() // 60)} minutes.\n"
|
||||
"If you didn't request this, ignore this message."
|
||||
),
|
||||
)
|
||||
# Send BEFORE commit (ecomm#7): a refused delivery rolls the row back, so no orphan
|
||||
# code blocks the 60s cooldown and the caller can honestly retry at once (INV-9).
|
||||
try:
|
||||
mailer.send(
|
||||
to=email,
|
||||
subject=f"Your ecomm code: {code}",
|
||||
body=(
|
||||
f"Your ecomm one-time code is {code}.\n"
|
||||
f"It is valid for {int(CODE_TTL.total_seconds() // 60)} minutes.\n"
|
||||
"If you didn't request this, ignore this message."
|
||||
),
|
||||
)
|
||||
except MailerError as exc:
|
||||
conn.rollback()
|
||||
raise DeliveryFailed(str(exc)) from exc
|
||||
conn.commit()
|
||||
|
||||
|
||||
def verify(conn: psycopg.Connection, email: str, code: str) -> tuple[Account, bool]:
|
||||
|
||||
Reference in New Issue
Block a user