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
+37 -37
View File
@@ -18,7 +18,7 @@
-webkit-font-smoothing: antialiased;
}
/* ── Nav Bar ───────────────────────────────────────────── */
/* Nav Bar */
.nav-bar {
position: sticky;
top: 0;
@@ -45,7 +45,7 @@
background-clip: text;
}
/* ── Hero ──────────────────────────────────────────────── */
/* Hero */
.hero {
padding: 48px 20px 32px;
text-align: center;
@@ -61,7 +61,7 @@
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 5px 14px;
border-radius: 20px;
border-radius: var(--radius-xl);
margin-bottom: 20px;
}
@@ -89,12 +89,12 @@
margin: 0 auto 28px;
}
/* ── Earnings Banner ───────────────────────────────────── */
/* Earnings Banner */
.earnings-card {
margin: 0 20px 32px;
background: linear-gradient(135deg, rgba(52,199,89,0.15) 0%, rgba(0,122,255,0.10) 100%);
border: 0.5px solid rgba(52,199,89,0.3);
border-radius: 18px;
border-radius: var(--radius-lg);
padding: 20px;
text-align: center;
}
@@ -113,7 +113,7 @@
margin-top: 4px;
}
/* ── 3-Step How It Works ───────────────────────────────── */
/* 3-Step How It Works */
.section-title {
font-family: var(--font-display, -apple-system, "SF Pro Display", sans-serif);
font-size: 22px;
@@ -137,7 +137,7 @@
gap: 16px;
background: rgba(255,255,255,0.06);
border: 0.5px solid rgba(255,255,255,0.1);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 18px 16px;
transition: background 0.2s;
}
@@ -149,7 +149,7 @@
.step-icon {
width: 44px;
height: 44px;
border-radius: 12px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -183,7 +183,7 @@
line-height: 1.4;
}
/* ── Live Missions ─────────────────────────────────────── */
/* Live Missions */
.missions-section {
margin-bottom: 36px;
}
@@ -208,7 +208,7 @@
content: '';
width: 8px;
height: 8px;
border-radius: 50%;
border-radius: var(--radius-full);
background: #34C759;
animation: pulse 2s infinite;
}
@@ -232,7 +232,7 @@
flex: 0 0 260px;
background: rgba(255,255,255,0.07);
border: 0.5px solid rgba(255,255,255,0.1);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
}
@@ -243,7 +243,7 @@
font-size: 12px;
font-weight: 600;
padding: 3px 10px;
border-radius: 10px;
border-radius: var(--radius-md);
margin-bottom: 10px;
}
@@ -279,7 +279,7 @@
color: rgba(52,199,89,0.7);
}
/* ── CTA Buttons ───────────────────────────────────────── */
/* CTA Buttons */
.cta-section {
padding: 0 20px;
margin-bottom: 40px;
@@ -294,7 +294,7 @@
background: #007AFF;
color: #fff;
border: none;
border-radius: 16px;
border-radius: var(--radius-lg);
font-family: var(--font-text, -apple-system, sans-serif);
font-size: 17px;
font-weight: 600;
@@ -316,7 +316,7 @@
background: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.85);
border: 0.5px solid rgba(255,255,255,0.15);
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 16px;
font-weight: 500;
cursor: pointer;
@@ -326,13 +326,13 @@
.btn-secondary:active { background: rgba(255,255,255,0.14); }
/* ── Stats Strip ───────────────────────────────────────── */
/* Stats Strip */
.stats-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: rgba(255,255,255,0.08);
border-radius: 16px;
border-radius: var(--radius-lg);
margin: 0 20px 36px;
overflow: hidden;
border: 0.5px solid rgba(255,255,255,0.1);
@@ -357,7 +357,7 @@
color: rgba(255,255,255,0.75);
}
/* ── FAQ / Trust ───────────────────────────────────────── */
/* FAQ / Trust */
.trust-section {
padding: 0 20px;
margin-bottom: 48px;
@@ -378,7 +378,7 @@
.trust-icon {
width: 32px;
height: 32px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -387,7 +387,7 @@
background: rgba(52,199,89,0.15);
}
/* ── Loading state for missions ────────────────────────── */
/* Loading state for missions */
.missions-loading {
padding: 0 20px;
color: rgba(255,255,255,0.75);
@@ -443,7 +443,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Hero -->
<section class="hero">
<div class="hero-badge">🌍 Launching August 2026</div>
<div class="hero-badge"> Launching August 2026</div>
<h1>Earn money on<br><span class="accent">what you see around you</span></h1>
<p>Take photo missions near you. Get paid fast. No boss, no invoicing.</p>
</section>
@@ -474,7 +474,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<p class="section-title">How it works</p>
<div class="steps">
<div class="step-card">
<div class="step-icon blue">🔍</div>
<div class="step-icon blue"><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></div>
<div class="step-content">
<div class="step-num">Step 1</div>
<div class="step-title">Find missions near you</div>
@@ -482,7 +482,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div class="step-card">
<div class="step-icon green">📸</div>
<div class="step-icon green"></div>
<div class="step-content">
<div class="step-num">Step 2</div>
<div class="step-title">Complete the mission</div>
@@ -490,7 +490,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
</div>
<div class="step-card">
<div class="step-icon purple">💰</div>
<div class="step-icon purple"></div>
<div class="step-content">
<div class="step-num">Step 3</div>
<div class="step-title">Get paid</div>
@@ -507,7 +507,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="missions-scroll" id="missions-container">
<div class="mission-card" style="opacity:0.5;text-align:center;padding:24px">
<div style="font-size:24px;margin-bottom:8px">📍</div>
<div style="font-size:24px;margin-bottom:8px"></div>
<div style="font-size:14px;color:#aaa">Loading missions near you...</div>
</div>
</div>
@@ -517,23 +517,23 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="trust-section">
<p class="section-title" style="padding:0;margin-bottom:12px">Always included</p>
<div class="trust-item">
<div class="trust-icon"></div>
<div class="trust-icon"><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></div>
Clear brief — you know exactly what to do
</div>
<div class="trust-item">
<div class="trust-icon">🔒</div>
<div class="trust-icon"></div>
Secure payout via Stripe Connect
</div>
<div class="trust-item">
<div class="trust-icon">📱</div>
<div class="trust-icon"></div>
No special gear — a smartphone is enough
</div>
<div class="trust-item">
<div class="trust-icon"></div>
<div class="trust-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
Approval and payment within 48 hours
</div>
<div class="trust-item">
<div class="trust-icon">🗺️</div>
<div class="trust-icon"></div>
Choose which missions you take
</div>
</div>
@@ -551,21 +551,21 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<script>
const API_BASE = 'https://api.quixzoom.com/api/qz';
function missionEmoji(t){
if(/elledn|kabel|transfor/i.test(t)) return '';
if(/vag|potthal|trottoar/i.test(t)) return '🛣️';
if(/kanal|bro/i.test(t)) return '🚤';
if(/gatuliv|neon/i.test(t)) return '🏙️';
return '📸';
if(/elledn|kabel|transfor/i.test(t)) return '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg>';
if(/vag|potthal|trottoar/i.test(t)) return '';
if(/kanal|bro/i.test(t)) return '';
if(/gatuliv|neon/i.test(t)) return '';
return '';
}
function renderMissions(missions){
const c=document.getElementById('missions-container');
if(!missions.length){c.innerHTML='<div style="text-align:center;padding:24px;color:#aaa;font-size:14px">📍 No missions near you right now</div>';return;}
if(!missions.length){c.innerHTML='<div style="text-align:center;padding:24px;color:#aaa;font-size:14px"> No missions near you right now</div>';return;}
c.innerHTML=missions.slice(0,6).map(m=>{
const d=m.distance_meters<1000?Math.round(m.distance_meters)+'m':(m.distance_meters/1000).toFixed(1)+'km';
return '<div class="mission-card" onclick="location.href=\''/zoomer-onboarding-flow\'" style="cursor:pointer">'
+'<div class="mission-type-badge road">'+missionEmoji(m.title)+' '+(m.area_name||'Mission')+'</div>'
+'<h3>'+m.title+'</h3>'
+'<div class="mission-meta">📍 '+d+' away · '+(m.required_photos||1)+' photo</div>'
+'<div class="mission-meta"> '+d+' away · '+(m.required_photos||1)+' photo</div>'
+'<div class="mission-reward">'+m.reward_credits+' cr<span> on approval</span></div>'
+'</div>';
}).join('');