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
+26 -26
View File
@@ -87,7 +87,7 @@
}
.section-card {
background: var(--background-secondary);
border-radius: 14px; overflow: hidden;
border-radius: var(--radius-lg); overflow: hidden;
border: 0.5px solid var(--separator);
}
@@ -99,7 +99,7 @@
}
.payout-row:last-child { border-bottom: none; }
.payout-icon {
width: 38px; height: 38px; border-radius: 10px;
width: 38px; height: 38px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px; flex-shrink: 0;
}
@@ -111,7 +111,7 @@
/* Connect card */
.connect-card {
background: var(--background-secondary);
border-radius: 14px;
border-radius: var(--radius-lg);
border: 0.5px solid var(--separator);
overflow: hidden;
}
@@ -120,7 +120,7 @@
display: flex; gap: 14px; align-items: center;
}
.connect-icon {
width: 48px; height: 48px; border-radius: 12px;
width: 48px; height: 48px; border-radius: var(--radius-md);
background: rgba(0,122,255,0.15);
display: flex; align-items: center; justify-content: center;
font-size: 24px; flex-shrink: 0;
@@ -145,7 +145,7 @@
display: flex; align-items: center; gap: 6px;
}
.status-dot {
width: 8px; height: 8px; border-radius: 50%;
width: 8px; height: 8px; border-radius: var(--radius-full);
}
.status-dot.active { background: var(--ios-green); }
.status-dot.pending { background: var(--ios-orange); }
@@ -164,7 +164,7 @@
/* Loader */
.loader { display:flex; justify-content:center; padding:40px; }
@keyframes spin { to { transform: rotate(360deg); } }
.spinner { width:28px; height:28px; border-radius:50%; border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
.spinner { width:28px; height:28px; border-radius: var(--radius-full); border:3px solid var(--fill-secondary); border-top-color:var(--ios-blue); animation:spin 0.8s linear infinite; }
/* Tab Bar */
.tab-bar {
@@ -185,7 +185,7 @@
.tab-item.active .tab-label { color: var(--ios-blue); }
/* Toast */
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius:20px; padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast { position:fixed; bottom:calc(env(safe-area-inset-bottom,34px)+100px); left:50%; transform:translateX(-50%) translateY(20px); background:var(--background-secondary); border:0.5px solid var(--separator); border-radius: var(--radius-xl); padding:10px 18px; font-size:14px; font-weight:500; box-shadow:0 8px 32px rgba(0,0,0,0.3); opacity:0; transition:all 0.3s; z-index:9999; white-space:nowrap; }
.toast.show { opacity:1; transform:translateX(-50%) translateY(0); }
@media (max-width: 640px) {
.hero h1 { font-size: 2rem !important; }
@@ -272,7 +272,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Payout account</div>
<div class="connect-card">
<div class="connect-top">
<div class="connect-icon">💳</div>
<div class="connect-icon"></div>
<div class="connect-info">
<div class="connect-title">Stripe Connect</div>
<div class="connect-desc">Verify your account to receive payouts directly to your bank</div>
@@ -283,7 +283,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
Loading account status...
</div>
<button class="connect-btn" id="connect-btn" onclick="openConnectOnboarding()">
🏦 Configure payout account →
Configure payout account →
</button>
</div>
</div>
@@ -293,7 +293,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Payout schedule</div>
<div class="section-card">
<div class="schedule-row">
<span class="schedule-label">🗓 Frekvens</span>
<span class="schedule-label"> Frekvens</span>
<span class="schedule-value">Every Monday</span>
</div>
<div class="schedule-row">
@@ -301,15 +301,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<span class="schedule-value">13 bankdagar</span>
</div>
<div class="schedule-row">
<span class="schedule-label">💳 Method</span>
<span class="schedule-label"> Method</span>
<span class="schedule-value">Stripe Connect</span>
</div>
<div class="schedule-row">
<span class="schedule-label">🏦 Currency</span>
<span class="schedule-label"> Currency</span>
<span class="schedule-value">SEK</span>
</div>
<div class="schedule-row">
<span class="schedule-label">📋 Min. payout</span>
<span class="schedule-label"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H10M6 8H10M6 11H8" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg> Min. payout</span>
<span class="schedule-value">500 kr</span>
</div>
</div>
@@ -321,15 +321,15 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Tab Bar -->
<nav class="tab-bar">
<a class="tab-item" href="zoomer-app.html">
<span class="tab-icon">🎯</span>
<span class="tab-icon"></span>
<span class="tab-label">Missions</span>
</a>
<a class="tab-item active" href="zoomer-earnings.html">
<span class="tab-icon">💰</span>
<span class="tab-icon"></span>
<span class="tab-label">Earnings</span>
</a>
<a class="tab-item" href="zoomer-profile.html">
<span class="tab-icon">👤</span>
<span class="tab-icon"></span>
<span class="tab-label">Profile</span>
</a>
</nav>
@@ -356,8 +356,8 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
function missionTypeEmoji(type) {
const m = { facade_scan:'🏢', roof_scan:'🏠', road_inspection:'🛣️', interior_scan:'🏚️', vegetation_survey:'🌳', inspection:'🔍' };
return m[type] || '📍';
const m = { facade_scan:'<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="2" width="10" height="12" rx="1" stroke="#666" stroke-width="1.5"/><path d="M6 5H7M6 8H7M6 11H7M9 5H10M9 8H10M9 11H10" stroke="#666" stroke-width="1.5" stroke-linecap="round"/></svg>', roof_scan:'', road_inspection:'', interior_scan:'', vegetation_survey:'', inspection:'<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>' };
return m[type] || '';
}
function missionTypeBg(type) {
@@ -397,7 +397,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (recent.length === 0) {
payoutsCard.innerHTML = `
<div style="padding:32px;text-align:center">
<div style="font-size:40px;margin-bottom:8px">💸</div>
<div style="font-size:40px;margin-bottom:8px"></div>
<div style="font-size:16px;font-weight:600;margin-bottom:4px">Inga utbetalningar än</div>
<div style="font-size:14px;color:var(--label-secondary)">Slutför ditt första uppdrag för att börja tjäna</div>
</div>`;
@@ -431,7 +431,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
console.error(e);
document.getElementById('payouts-card').innerHTML = `
<div style="padding:24px;text-align:center;color:var(--label-secondary)">
<div>⚠️ Could not load earnings data</div>
<div><svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Could not load earnings data</div>
</div>`;
}
}
@@ -446,7 +446,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
const data = await r.json();
statusEl.innerHTML = `<div class="status-dot active"></div> Account activated — ready for payouts`;
btnEl.textContent = '⚙️ Hantera konto →';
btnEl.textContent = '<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> Hantera konto →';
if (data.url) { btnEl.dataset.url = data.url; }
} else if (r.status === 401) {
statusEl.innerHTML = `<div class="status-dot pending"></div> Inloggning krävs för att verifiera konto`;
@@ -473,16 +473,16 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (r.ok) {
const data = await r.json();
if (data.url) { window.open(data.url, '_blank'); }
else { toast(' Konto verifierat!'); }
else { toast('<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> Konto verifierat!'); }
} else if (r.status === 401) {
toast('🔐 Behörighet saknas — kontakta support');
toast('<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="3" y="7" width="10" height="7" rx="1" stroke="#666" stroke-width="1.5"/><path d="M5 7V5C5 3.3 6.3 2 8 2C9.7 2 11 3.3 11 5V7" stroke="#666" stroke-width="1.5"/><circle cx="8" cy="10.5" r="1" fill="#666"/></svg> Behörighet saknas — kontakta support');
} else {
toast('⚠️ Stripe Connect onboarding — kontakta support@quixzoom.com');
toast('<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 3L17 17H3L10 3Z" stroke="#f59e0b" stroke-width="2"/><path d="M10 8V12M10 14V14.5" stroke="#f59e0b" stroke-width="2" stroke-linecap="round"/></svg> Stripe Connect onboarding — kontakta support@quixzoom.com');
}
} catch {
toast(' Contact support to configure your payout account');
toast(' Contact support to configure your payout account');
} finally {
btn.textContent = '🏦 Configure payout account →';
btn.textContent = ' Configure payout account →';
btn.disabled = false;
}
}