pdf-quiz-generator/backend/app/models
Daniel 25a9a8aca4 feat: sign in with a code sent by email
A password is a thing to remember and a thing to lose. Somebody who can read
their own mail can now sign in without one: ask, receive six characters, type
them into the page that is already open.

A code rather than a link, and the difference is not cosmetic. The token in a
link was 256 bits, unguessable however long it lived, so its length, its expiry
and its rate limit were three independent decisions. Six characters is 2^30,
and the three stop being independent — so they are argued together:

  * six characters of the invite alphabet, imported rather than copied, because
    there should be one answer to which characters a person may be asked to
    retype and that one already drops O/0 and I/1;
  * a code answers five guesses and is then retired, not slowed — whoever is
    typing has lost the mail or does not own it, and both are one click from a
    new one;
  * one code live per person, since several would mean one guess tested against
    all of them;
  * ten verify attempts per address per fifteen minutes, so nobody buys five
    fresh guesses at a time by asking again.

Tens of guesses an hour against a billion, and the victim gets a mail for every
code burned. Eight characters would buy a thousandfold against an attack the
guess budget has already ended, and cost every person two more characters.

The attempt count lives in the row, not the cache. The Redis limiter fails open
when Redis is down, which is right for what it usually guards and wrong for the
only thing standing between a patient stranger and six characters.

Verifying is scoped to the address. A short code looked up on its own would be
tried against every code live on the site at once — the short code's one real
weakness, closed by knowing whose code it should be before comparing.

Fifteen minutes, because a first mail between strangers is routinely greylisted
five to ten and a code that expires before it arrives is not a sign-in method.
Shortening it buys nothing: one code is live and it answers five guesses
however long it sits there.

Nothing distinguishes an address with an account from one without — same
message, same status, same duration, and both rate limits counted before the
account is looked up, so a 429 cannot become the tell. Redis keys are
fingerprints, and the table holds a fingerprint rather than the code.

SSO stays first where it is configured, and a password is still one click away
for anybody who has one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 17:29:28 +02:00
..
__init__.py feat: sign in with a code sent by email 2026-09-12 17:29:28 +02:00
ai_model_config.py Tag filtering, multi-category, bug fixes, image validation, docs 2026-04-04 22:48:26 +02:00
article.py feat: article CMS, three reading views, and articles written from the library 2026-09-10 17:13:07 +02:00
attempt.py feat: right after a tip is its own slice 2026-09-12 01:51:03 +02:00
category_grant.py feat: grants can name an exam and a discipline, not only a category 2026-09-10 04:02:36 +02:00
collection.py feat: articles are collectible, and Summary is a toggle 2026-09-12 14:52:20 +02:00
conversation.py feat: AI Mode — a chat that cannot cite what it did not find 2026-09-10 15:19:24 +02:00
course.py Add LMS, user quiz creation, manual questions, course system 2026-04-05 02:07:04 +02:00
draft_question.py feat: extracted questions are staged, and only acceptance takes an id 2026-09-11 22:57:08 +02:00
email_verification.py Major platform update: pgvector search, multi-provider TTS, settings page, CLI 2026-03-31 18:03:10 +02:00
embeddable.py feat: hybrid search for articles and cards; full-page question editor 2026-09-10 02:01:35 +02:00
exam.py feat: exams are a group you can build, with the board's own blueprint 2026-09-11 20:27:48 +02:00
favorite.py Add switchable embedding model, Polly toggle, job cancellation, and UI fixes 2026-04-03 20:44:11 +02:00
feedback.py feat: feedback replaces comments; Qbank is a landing page; one question page 2026-09-11 18:50:57 +02:00
flashcard.py feat: hybrid search for articles and cards; full-page question editor 2026-09-10 02:01:35 +02:00
invite.py feat: the player is a box; sharing and sign-up are the administrator's 2026-09-11 19:17:35 +02:00
lab_reference.py feat: Orthobullets-style lab panel with source deep links and card links 2026-09-08 16:20:55 +02:00
login_code.py feat: sign in with a code sent by email 2026-09-12 17:29:28 +02:00
media.py feat: MinIO-backed media libraries, and file the last 316 questions 2026-09-10 06:45:22 +02:00
password_reset.py Major platform update: pgvector search, multi-provider TTS, settings page, CLI 2026-03-31 18:03:10 +02:00
pdf_document.py Major platform update: pgvector search, multi-provider TTS, settings page, CLI 2026-03-31 18:03:10 +02:00
question.py refactor: remove per-question sharing 2026-09-12 08:42:51 +02:00
question_category.py feat: an organ system is a fact about a topic, not about a keyword 2026-09-12 04:32:25 +02:00
question_media.py feat: figures as records, question-centred dashboard, fewer hints mid-quiz 2026-09-11 03:18:49 +02:00
quiz.py feat: a repetition does not raise your score; no more deleting a session 2026-09-12 01:08:42 +02:00
quiz_category.py Major: categories, question bank, security fixes, mobile layout, UX improvements 2026-03-31 20:08:05 +02:00
quiz_question_link.py Junction table for shared questions; verification blocking; semantic bank search; bug fixes 2026-04-01 00:55:13 +02:00
section.py CRITICAL: Remove cascade delete from Section→Quiz relationship 2026-04-01 18:26:22 +02:00
study_plan.py feat: study plans you can open, work through, and edit 2026-09-10 12:10:39 +02:00
user.py feat: a reading voice in Settings, and a tutor that reads every figure 2026-09-12 16:48:09 +02:00
user_note.py feat: attending tip, per-question notes, save to folder, session clock 2026-09-11 02:59:32 +02:00