auth: affirmative versioned sign-up acknowledgement (EN + IT)
Replaces the passive "by signing in you agree…" paragraph on /login with
an active un-pre-ticked checkbox over three short statements that the
user actively accepts. Each acceptance is recorded against a code-level
version pointer, so a future wording revision bumps the version and
prompts every user again.
- New app/legal.py::ACKNOWLEDGEMENT_VERSION (currently 1).
- New UserAcknowledgement model + migration 0027 (user_id FK CASCADE,
version, lang, accepted_at, composite index on user_id+version).
- auth_service: has_acknowledged_current() and record_acknowledgement()
helpers; POST /login validates the checkbox, falls through to a 400
with the localised error otherwise, and writes a row iff the user has
no current-version row (so existing-already-accepted users don't
produce duplicates).
- GET /login: language detection mirrors the landing's
detect_public_lang(); ?lang=en|it overrides; stamps the rtm.lang
cookie; passes the locale dict + version into the template.
- login.html: EN/IT pill, localised lede/banner/legal footer, required
checkbox in an acknowledgement block, hidden lang+ack_version fields.
Submit disabled until the box is ticked (UX polish; the server check
is what carries weight).
- locales/{en,it}.yaml: new auth.ack.* section with TODO(legal) marker.
Wording matches the brief's substance pending solicitor sign-off.
- tests/test_signup_acknowledgement.py: 10 tests (EN + IT rejection,
one row per acceptance, displayed-lang recorded, idempotent on
current version, version-bump writes new row, helper unit tests).
The acknowledgement strengthens the user-civil-claim vector — combined
with the liability cap and the Ltd, it makes "I was misled into thinking
this was advice" much harder to argue. It does NOT move the regulatory
perimeter, which is governed by the content discipline shipped in
47dce1a. Belt-and-braces, not a substitute.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
47dce1a1a4
commit
f3ac65f8f7
9 changed files with 714 additions and 13 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="{{ lang or 'en' }}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
|
@ -17,40 +17,110 @@
|
|||
<body>
|
||||
<div class="auth-shell">
|
||||
<div class="auth-card">
|
||||
<div class="auth-card__brand">{{ BRAND_NAME }}</div>
|
||||
<div class="auth-card__hint">sign in with email</div>
|
||||
<div class="auth-card__brand" style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<span>{{ BRAND_NAME }}</span>
|
||||
<span class="auth-card__lang-switch" role="group"
|
||||
aria-label="{{ (t and t.auth.ack.lang_switch_aria) or 'Language' }}"
|
||||
style="font-size:11px; letter-spacing:0.04em;">
|
||||
<a href="/login?lang=en{% if next_path and next_path != '/' %}&next={{ next_path }}{% endif %}{% if ref %}&ref={{ ref }}{% endif %}"
|
||||
style="text-decoration:none; opacity:{{ '1' if lang == 'en' else '0.55' }};">EN</a>
|
||||
<span style="opacity:0.4;">·</span>
|
||||
<a href="/login?lang=it{% if next_path and next_path != '/' %}&next={{ next_path }}{% endif %}{% if ref %}&ref={{ ref }}{% endif %}"
|
||||
style="text-decoration:none; opacity:{{ '1' if lang == 'it' else '0.55' }};">IT</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="auth-card__hint">
|
||||
{% if lang == 'it' %}accedi via email{% else %}sign in with email{% endif %}
|
||||
</div>
|
||||
|
||||
{% if referrer_present %}
|
||||
<div class="auth-info auth-info--invited">
|
||||
<strong>You've been invited.</strong>
|
||||
<strong>{% if lang == 'it' %}Sei stato invitato.{% else %}You've been invited.{% endif %}</strong>
|
||||
{% if lang == 'it' %}
|
||||
Quando ti abboni, tu e il tuo amico ricevete entrambi
|
||||
<strong>50% di sconto per 3 mesi</strong>. Iscriviti qui sotto per attivarlo.
|
||||
{% else %}
|
||||
When you subscribe, you and your friend both get
|
||||
<strong>50% off for 3 months</strong>. Sign up below to lock it in.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<p class="auth-card__lede">
|
||||
{% if lang == 'it' %}
|
||||
Inserisci la tua email e ti invieremo un codice di 6 cifre. Niente password.
|
||||
I nuovi visitatori creano un account; chi torna fa l'accesso.
|
||||
{% else %}
|
||||
Enter your email and we'll send you a 6-digit code. No password.
|
||||
First-time visitors get an account; returning visitors get a sign-in.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if error %}<div class="auth-error">{{ error }}</div>{% endif %}
|
||||
|
||||
<form method="post" action="/login" autocomplete="on">
|
||||
<input type="hidden" name="next" value="{{ next_path or '/' }}">
|
||||
<input type="hidden" name="lang" value="{{ lang or 'en' }}">
|
||||
<input type="hidden" name="ack_version" value="{{ ack_version }}">
|
||||
{% if ref %}<input type="hidden" name="ref" value="{{ ref }}">{% endif %}
|
||||
<label>Email
|
||||
<input type="email" name="email" value="{{ email or '' }}" required autofocus>
|
||||
</label>
|
||||
<button type="submit">Send code</button>
|
||||
|
||||
{# Affirmative un-pre-ticked acknowledgement. The substance is
|
||||
locale-driven (auth.ack.*); the checkbox is required client-side
|
||||
AND server-side. Replaces a passive "by continuing you agree…"
|
||||
paragraph because an active acceptance carries more weight. #}
|
||||
{% if t %}
|
||||
<div class="auth-ack" style="margin-top:18px; padding:14px 16px;
|
||||
border:1px solid var(--border, #ddd); border-radius:6px;
|
||||
font-size:12.5px; line-height:1.55;">
|
||||
<div style="font-weight:600; margin-bottom:8px;">{{ t.auth.ack.heading }}</div>
|
||||
<ul style="margin:0 0 12px 18px; padding:0;">
|
||||
{% for item in t.auth.ack.items %}
|
||||
<li style="margin-bottom:6px;">{{ item }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<label style="display:flex; gap:8px; align-items:flex-start; cursor:pointer;">
|
||||
<input type="checkbox" name="acknowledged" id="ack-box"
|
||||
value="on" required style="margin-top:3px;">
|
||||
<span>{{ t.auth.ack.checkbox_label }}</span>
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" id="ack-submit" disabled
|
||||
style="margin-top:14px;">
|
||||
{% if lang == 'it' %}Invia codice{% else %}Send code{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-card__legal" style="margin-top:18px; font-size:11px; color: var(--muted); line-height:1.6;">
|
||||
By signing in you agree to our
|
||||
<a href="/terms">Terms</a> and
|
||||
<a href="/privacy">Privacy notice</a>, and confirm you’ve read
|
||||
the <a href="/disclaimer">financial disclaimer</a>.
|
||||
{% if lang == 'it' %}
|
||||
Vedi i nostri
|
||||
<a href="/terms">Termini</a>, l’<a href="/privacy">Informativa privacy</a>
|
||||
e il <a href="/disclaimer">disclaimer finanziario</a>.
|
||||
{% else %}
|
||||
See our
|
||||
<a href="/terms">Terms</a>,
|
||||
<a href="/privacy">Privacy notice</a>, and
|
||||
<a href="/disclaimer">financial disclaimer</a>.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Tiny UX polish: disable the submit until the box is ticked. The
|
||||
// server-side validation is what protects us — this is just so the
|
||||
// user can't burn a click and immediately see an error.
|
||||
(function () {
|
||||
var box = document.getElementById('ack-box');
|
||||
var btn = document.getElementById('ack-submit');
|
||||
if (!box || !btn) return;
|
||||
function sync() { btn.disabled = !box.checked; }
|
||||
box.addEventListener('change', sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue