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
+46 -46
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Launch Readiness — quiXzoom 1 augusti 2026</title>
<style>
/* ── iOS 26 / Apple Native — Light Mode ──────────────────────────────── */
/* iOS 26 / Apple Native — Light Mode */
:root {
--bg: #f2f2f7;
--bg-card: #ffffff;
@@ -41,7 +41,7 @@
padding-bottom: calc(var(--safe-bottom) + 60px);
}
/* ── Nav Bar ─────────────────────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -62,18 +62,18 @@
font-size: 13px;
font-weight: 600;
padding: 4px 12px;
border-radius: 20px;
border-radius: var(--radius-xl);
transition: background 0.3s;
}
.nav-badge.ready { background: var(--green); }
.nav-badge.warn { background: var(--orange); }
/* ── Page Header ─────────────────────────────────────────────────────── */
/* Page Header */
.page-header { padding: 24px 20px 8px; }
.page-title { font-size: 34px; font-weight: 700; letter-spacing: -0.5px; line-height: 1.1; }
.page-sub { font-size: 15px; color: var(--label-secondary); margin-top: 4px; }
/* ── Countdown Hero ──────────────────────────────────────────────────── */
/* Countdown Hero */
.countdown-hero {
margin: 16px 16px 0;
background: linear-gradient(135deg, #001E50 0%, #0040A8 100%);
@@ -98,7 +98,7 @@
.cd-block {
flex: 1;
background: rgba(255,255,255,0.12);
border-radius: 10px;
border-radius: var(--radius-md);
padding: 12px 6px 10px;
text-align: center;
}
@@ -131,7 +131,7 @@
}
.countdown-launch strong { color: #fff; font-weight: 700; }
/* ── Progress Hero ───────────────────────────────────────────────────── */
/* Progress Hero */
.progress-hero {
margin: 12px 16px 0;
background: var(--bg-card);
@@ -160,18 +160,18 @@
.days-left.green { color: var(--green); }
.progress-bar-bg {
background: var(--fill);
border-radius: 4px;
border-radius: var(--radius-sm);
height: 10px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--tint), #5AC8FA);
transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* ── Stat Row ────────────────────────────────────────────────────────── */
/* Stat Row */
.stat-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -204,7 +204,7 @@
margin-top: 2px;
}
/* ── Final Verification Banner ───────────────────────────────────────── */
/* Final Verification Banner */
.verify-banner {
margin: 22px 16px 0;
background: rgba(52,199,89,0.10);
@@ -233,7 +233,7 @@
.verify-row:last-child { border-bottom: none; padding-bottom: 0; }
.verify-dot {
width: 20px; height: 20px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 11px;
font-weight: 700;
@@ -249,10 +249,10 @@
color: var(--label-secondary);
background: var(--fill);
padding: 2px 6px;
border-radius: 4px;
border-radius: var(--radius-sm);
}
/* ── Category Section ────────────────────────────────────────────────── */
/* Category Section */
.cat-section { margin-top: 28px; }
.cat-header {
display: flex;
@@ -262,7 +262,7 @@
}
.cat-icon {
width: 28px; height: 28px;
border-radius: 7px;
border-radius: var(--radius-sm);
display: flex; align-items: center; justify-content: center;
font-size: 14px;
flex-shrink: 0;
@@ -282,18 +282,18 @@
}
.cat-progress-bar {
height: 3px;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--fill);
margin: 0 20px 8px;
overflow: hidden;
}
.cat-progress-fill {
height: 100%;
border-radius: 2px;
border-radius: var(--radius-sm);
transition: width 0.6s ease;
}
/* ── Checklist ───────────────────────────────────────────────────────── */
/* Checklist */
.checklist {
background: var(--bg-card);
border-radius: var(--radius-lg);
@@ -316,7 +316,7 @@
.check-circle {
width: 24px; height: 24px;
border-radius: 50%;
border-radius: var(--radius-full);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 13px;
@@ -354,7 +354,7 @@
font-size: 12px;
font-weight: 500;
padding: 2px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: var(--fill);
color: var(--label-secondary);
}
@@ -377,7 +377,7 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
background: var(--fill);
color: var(--label-secondary);
}
@@ -385,14 +385,14 @@
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.tag-done { background: rgba(52,199,89,0.15); color: var(--green); }
.tag-inprog { background: rgba(0,122,255,0.12); color: var(--tint); }
.tag-blocked { background: rgba(255,59,48,0.12); color: var(--red); }
.tag-pending { background: var(--fill); color: var(--label-tertiary); }
/* ── Footer ──────────────────────────────────────────────────────────── */
/* Footer */
.footer {
text-align: center;
padding: 32px 20px 12px;
@@ -434,7 +434,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Nav Bar -->
<div class="nav-bar">
<div class="nav-title">🚀 Launch Readiness</div>
<div class="nav-title"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg> Launch Readiness</div>
<div class="nav-badge" id="navBadge">Laddar…</div>
</div>
@@ -514,38 +514,38 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Final Verification Section -->
<div style="margin:22px 16px 0">
<div style="font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:0.6px;color:var(--label-secondary);padding:0 4px 8px;display:flex;align-items:center;gap:6px;">
<span style="font-size:16px"></span> Final Verification — W11-QUIXZOOM-LAUNCH
<span style="font-size:16px"><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#22c55e" stroke-width="2"/><path d="M6 10L9 13L14 7" stroke="#22c55e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span> Final Verification — W11-QUIXZOOM-LAUNCH
</div>
</div>
<div class="verify-banner">
<div class="verify-banner-title"><span>🔍</span> 5 kritiska kontrollpunkter</div>
<div class="verify-banner-title"><span><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="5" stroke="#666" stroke-width="2"/><path d="M11 11L14 14" stroke="#666" stroke-width="2" stroke-linecap="round"/></svg></span> 5 kritiska kontrollpunkter</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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></div>
<div class="verify-text">Alla quiXzoom API-endpoints svarar</div>
<div class="verify-endpoint">14 moduler · /api/quixzoom/*</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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></div>
<div class="verify-text">Zoomer-registrering end-to-end</div>
<div class="verify-endpoint">POST /zoomers/register → KYC → godkänd</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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></div>
<div class="verify-text">Mission create / accept / complete-flöde</div>
<div class="verify-endpoint">draft → open → active → completed</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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></div>
<div class="verify-text">Payout-automation aktiv (Stripe Connect)</div>
<div class="verify-endpoint">POST /payouts/process · 85% Zoomer / 15% plattform</div>
</div>
<div class="verify-row">
<div class="verify-dot vdot-ok"></div>
<div class="verify-dot vdot-ok"><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></div>
<div class="verify-text">DAC7 XML-export fungerar</div>
<div class="verify-endpoint">GET /compliance/dac7/report?format=xml · Skatteverket-schema</div>
</div>
@@ -562,7 +562,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<script>
const LAUNCH = new Date('2026-06-15T09:00:00+02:00');
// ── Countdown ─────────────────────────────────────────────────────────────
// Countdown
function pad(n) { return String(n).padStart(2, '0'); }
function tickCountdown() {
@@ -591,7 +591,7 @@ const CATEGORIES = [
{
id: 'verification',
name: 'API Verification',
icon: '⚙️',
icon: '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="3" stroke="#666" stroke-width="1.5"/><path d="M8 2V4M8 12V14M2 8H4M12 8H14M4.3 4.3L5.7 5.7M10.3 10.3L11.7 11.7M4.3 11.7L5.7 10.3M10.3 5.7L11.7 4.3" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>',
color: '#007AFF',
items: [
{ title: 'missions.mjs — create/accept/complete lifecycle', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /missions + PATCH /:id/status · statusmaskin verifierad' },
@@ -609,7 +609,7 @@ const CATEGORIES = [
{
id: 'zoomer',
name: 'Zoomer-flöde',
icon: '🧑‍💼',
icon: '',
color: '#34C759',
items: [
{ title: 'Zoomer-registrering komplett (6-stegs KYC)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'Personuppgifter → ID → Adress → Bankkonto → Skatteform → Villkor' },
@@ -623,7 +623,7 @@ const CATEGORIES = [
{
id: 'mission',
name: 'Mission-flöde',
icon: '🎯',
icon: '',
color: '#FF9500',
items: [
{ title: 'Customer bokar uppdrag (request-mission-flöde)', owner: 'Backend', status: 'done', deadline: 'Juni 10', note: 'POST /customers/:id/book → mission draft' },
@@ -638,7 +638,7 @@ const CATEGORIES = [
{
id: 'payout',
name: 'Utbetalningar & DAC7',
icon: '💳',
icon: '',
color: '#AF52DE',
items: [
{ title: 'Stripe Production Keys konfigurerade', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
@@ -654,7 +654,7 @@ const CATEGORIES = [
{
id: 'ios',
name: 'iOS Build',
icon: '📱',
icon: '',
color: '#5AC8FA',
items: [
{ title: 'Mac SSH-access för iOS builds löst', owner: 'Johan', status: 'done', deadline: 'Juni 1', note: 'Eskalerat → löst efter Sven-intervention' },
@@ -668,7 +668,7 @@ const CATEGORIES = [
{
id: 'appstore',
name: 'App Store',
icon: '🏪',
icon: '',
color: '#007AFF',
items: [
{ title: 'App Store Connect-konto verifierat', owner: 'Ops', status: 'done', deadline: 'Maj 10' },
@@ -682,7 +682,7 @@ const CATEGORIES = [
{
id: 'legal',
name: 'Legal / Compliance',
icon: '⚖️',
icon: '',
color: '#FF3B30',
items: [
{ title: 'Privacy Policy + Terms of Service publicerade', owner: 'Legal', status: 'done', deadline: 'Maj 15' },
@@ -695,7 +695,7 @@ const CATEGORIES = [
{
id: 'operations',
name: 'Operations',
icon: '🔧',
icon: '',
color: '#FF9500',
items: [
{ title: 'Monitoring & alerting konfigurerat', owner: 'Ops', status: 'done', deadline: 'Juni 5' },
@@ -710,7 +710,7 @@ const CATEGORIES = [
{
id: 'marketing',
name: 'Marketing',
icon: '📣',
icon: '',
color: '#34C759',
items: [
{ title: 'Landing page quixzoom.se live', owner: 'Mktg', status: 'done', deadline: 'Maj 20' },
@@ -723,7 +723,7 @@ const CATEGORIES = [
},
];
// ── Helpers ───────────────────────────────────────────────────────────────
// Helpers
function computeDaysLeft() {
const diff = LAUNCH - new Date();
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)));
@@ -736,9 +736,9 @@ function statusCircleClass(s) {
return 'pending';
}
function statusCircleContent(s) {
if (s === 'done') return '';
if (s === 'done') return '<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>';
if (s === 'in-prog') return '→';
if (s === 'blocked') return '';
if (s === 'blocked') return '';
return '';
}
function statusTagClass(s) {
@@ -754,7 +754,7 @@ function statusTagLabel(s) {
return 'Pending';
}
// ── Render ────────────────────────────────────────────────────────────────
// Render
function render() {
let totalDone = 0, totalInprog = 0, totalBlocked = 0, totalPending = 0, totalItems = 0;
@@ -777,7 +777,7 @@ function render() {
document.getElementById('totalItems').textContent = totalItems;
const daysEl = document.getElementById('daysLeft');
daysEl.textContent = days > 0 ? `${days} dagar kvar` : 'LAUNCH DAY 🚀';
daysEl.textContent = days > 0 ? `${days} dagar kvar` : 'LAUNCH DAY <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 2L10 8L14 9L8 14L6 8L2 7L8 2Z" fill="#666"/></svg>';
if (days <= 3) daysEl.style.color = 'var(--red)';
document.getElementById('cntDone').textContent = totalDone;