feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
@@ -167,7 +167,7 @@
.footer-links a { font-size: 14px; color: var(--text-muted); transition: color 0.2s; }
.footer-links a:hover { color: #1d1d1f; }
.footer-social {
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: 8px;
width: 32px; height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
@@ -187,7 +187,7 @@
[style*="grid-template-columns:repeat(auto-fit,minmax(260px"] { grid-template-columns: 1fr !important; }
}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius:8px;line-height:1.6;max-width:800px}
.lv-disclaimer{font-size:.75rem;color:rgba(0,0,0,.75);margin-top:24px;padding:12px 16px;background:rgba(0,0,0,.04);border-radius: var(--radius-md);line-height:1.6;max-width:800px}
.lv-disclaimer a{color:rgba(0,0,0,.65);text-decoration:underline}
.lv-disclaimer--light{color:#888;background:#f5f5f5}
.lv-disclaimer--light a{color:#555}
@@ -263,32 +263,32 @@
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:20px">
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Physical exposure score</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Composite score of observable physical risk factors — drainage, elevation, construction quality, age.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🌊</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Flood/weather risk signals</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Observed drainage capacity, flood markers, historical damage evidence and climate exposure indicators.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🏗️</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 14H14M4 14V8L8 4L12 8V14" stroke="#666" stroke-width="1.5" stroke-linejoin="round"/><path d="M6 14V10H10V14" stroke="#666" stroke-width="1.5"/></svg></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Infrastructure vulnerability</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Condition and resilience of critical infrastructure serving the area — power, water, roads, communications.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🤝</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Social resilience indicators</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Observable signals of community capacity and social infrastructure — services, connectivity, density.</p>
</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px">🔄</div>
<div style="background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:24px 28px;cursor:pointer" onclick="location.href='#contact'" role="button" tabindex="0">
<div style="font-size:1.5rem;margin-bottom:10px"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M2 8C2 11.3 4.7 14 8 14C10.5 14 12.7 12.5 13.7 10.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M14 8C14 4.7 11.3 2 8 2C5.5 2 3.3 3.5 2.3 5.7" stroke="#666" stroke-width="1.5" stroke-linecap="round"/><path d="M13.5 7.5L14 10L11.5 9.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M2.5 8.5L2 6L4.5 6.5" stroke="#666" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg></div>
<h3 style="font-size:1rem;font-weight:700;color:#0a0a0a;margin-bottom:6px">Recovery capacity</h3>
<p style="font-size:.875rem;color:#6b7280;line-height:1.6">Speed of observable recovery from prior disruption events — maintenance responsiveness and institutional capacity.</p>
</div>
@@ -303,9 +303,9 @@
<div class="section-label">Use cases</div>
<h2 class="section-title section-title-light" style="margin-bottom:24px">Who uses this index.</h2>
<div style="display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-bottom:40px">
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Insurance underwriting</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Portfolio stress testing</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius:100px">Resilience planning</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Insurance underwriting</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Portfolio stress testing</span>
<span style="background:#e0eaff;color:#0052cc;font-size:.8125rem;font-weight:600;padding:6px 14px;border-radius: 100px">Resilience planning</span>
</div>
</div>
</div>
@@ -318,7 +318,7 @@
<div class="section-label">Sample output</div>
<h2 class="section-title" style="margin-bottom:8px">What you receive.</h2>
<p style="color:var(--text-dim);margin-bottom:48px">Risk score + exposure map + resilience rating</p>
<div style="background:var(--surface-2);border:1px solid var(--border);border-radius:16px;padding:36px 40px;text-align:left">
<div style="background:var(--surface-2);border:1px solid var(--border);border-radius: var(--radius-lg);padding:36px 40px;text-align:left">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<span style="color:var(--text-dim);font-size:.875rem;font-weight:600">Sample location · Placeholder score</span>
<span style="color:#fbbf24;font-size:.875rem;font-weight:700">→ Stable risk</span>
@@ -328,8 +328,8 @@
<span style="font-size:1.5rem;color:var(--text-dim)">/100</span>
</div>
<div style="background:#f3f4f6;border-radius:8px;height:12px;width:100%;margin:12px 0 8px;overflow:hidden">
<div style="height:100%;width:38%;background:#f87171;border-radius:8px;transition:width 1s ease"></div>
<div style="background:#f3f4f6;border-radius: var(--radius-md);height:12px;width:100%;margin:12px 0 8px;overflow:hidden">
<div style="height:100%;width:38%;background:#f87171;border-radius: var(--radius-md);transition:width 1s ease"></div>
</div>
<div style="display:flex;justify-content:space-between;font-size:.75rem;color:var(--text-dim)">
@@ -352,29 +352,29 @@
<h2 class="section-title">How this index detects narrative conflicts.</h2>
<p style="color:rgba(255,255,255,.75);max-width:600px;margin:0 auto;line-height:1.7">Risk & Resilience Profile detects contradictions between official risk assessments and observed physical conditions. When flood zone maps say low risk but field observations show inadequate drainage, ageing embankments and deferred maintenance — the contradiction engine raises the actual risk score above the official rating.</p>
</div>
<div style="background:rgba(0,0,0,.05);border:1px solid rgba(0,0,0,.1);border-radius:16px;padding:32px 36px">
<div style="background:rgba(0,0,0,.05);border:1px solid rgba(0,0,0,.1);border-radius: var(--radius-lg);padding:32px 36px">
<div style="display:flex;align-items:flex-start;gap:16px;margin-bottom:24px">
<span style="font-size:1.5rem">🚩</span>
<span style="font-size:1.5rem"></span>
<div>
<div style="color:#f87171;font-weight:800;font-size:.9375rem;margin-bottom:4px">Contradiction Engine — How it works</div>
<div style="color:rgba(255,255,255,.75);font-size:.875rem;line-height:1.6">Field observations are continuously cross-referenced against official records, registers, and published statistics. Divergence above threshold triggers a conflict flag.</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
<div style="background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);border-radius:10px;padding:16px">
<div style="background:rgba(34,197,94,.1);border:1px solid rgba(34,197,94,.2);border-radius: var(--radius-md);padding:16px">
<div style="color:#86efac;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Official record</div>
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px">
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Published condition ratings</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Official statistics</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Register data</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Published condition ratings</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Official statistics</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Register data</li>
</ul>
</div>
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);border-radius:10px;padding:16px">
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.2);border-radius: var(--radius-md);padding:16px">
<div style="color:#fca5a5;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Observed reality</div>
<ul style="list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px">
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Field observations</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Real-time signals</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"> Contradiction flags</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Field observations</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Real-time signals</li>
<li style="color:rgba(255,255,255,.7);font-size:.875rem"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> Contradiction flags</li>
</ul>
</div>
</div>
@@ -392,20 +392,20 @@
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:20px;max-width:900px;margin:0 auto">
<a href="/intelligence/infrastructure-risk-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<a href="/intelligence/infrastructure-risk-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">Infrastructure Risk Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
<a href="/intelligence/city-health-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px">🏙️</div>
<a href="/intelligence/city-health-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">City Health Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
<a href="/intelligence/neighbourhood-quality-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius:12px;padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px">🏘️</div>
<a href="/intelligence/neighbourhood-quality-index/" style="display:block;background:#f8f9ff;border:1px solid #e5e7eb;border-radius: var(--radius-md);padding:20px 24px;text-decoration:none;color:inherit;transition:border-color .2s,transform .15s" onmouseover="this.style.borderColor='#0066ff';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='#e5e7eb';this.style.transform='none'">
<div style="font-size:1.25rem;margin-bottom:8px"></div>
<div style="font-weight:700;color:#0a0a0a;font-size:.9375rem;margin-bottom:4px">Neighbourhood Quality Index</div>
<div style="color:#0066ff;font-size:.8125rem;font-weight:700">View index →</div>
</a>
@@ -503,7 +503,7 @@
});
if (res.ok || res.status === 201) {
msgEl.className = 'success';
msgEl.textContent = " Request received. We'll be in touch within one business day.";
msgEl.textContent = "<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M3 8L6.5 11.5L13 4.5" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg> Request received. We'll be in touch within one business day.";
msgEl.style.display = 'block';
this.reset();
} else { throw new Error('Server error ' + res.status); }