from datetime import datetime from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from app.database import Base class LoginCode(Base): """One issued sign-in code. Only the fingerprint of the code is kept. A row here is a session waiting to happen — anybody who could read the table could sign in as its owner — so it holds something to compare against rather than something to type in. The failed guesses are counted in this row and not in Redis. The rate limiter fails open when the cache is down, which is the right call for anything it normally protects; it is the wrong call for the only thing standing between a patient stranger and six characters. """ __tablename__ = "login_codes" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) #: SHA-256 of the normalised code, hex. Not bcrypt: the guess budget is #: what makes a short code safe, not the cost of testing one, and this is #: compared on a path a person is waiting on. Not indexed and not unique #: either — a code is only ever looked up against the one account it was #: issued for, and six characters do collide. code_hash = Column(String(64), nullable=False) attempts = Column(Integer, nullable=False, default=0) expires_at = Column(DateTime, nullable=False) #: Set when the code is spent, when its guesses run out, and when a later #: request retires it. All three mean the same thing to every reader here: #: no longer usable. consumed_at = Column(DateTime, nullable=True) created_at = Column(DateTime, default=datetime.utcnow)