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
+24 -24
View File
@@ -30,12 +30,12 @@
.nav-subtitle { font-size: 12px; color: var(--label-secondary); margin-top: 2px; }
.status-pill {
display: flex; align-items: center; gap: 6px;
padding: 4px 12px; border-radius: 20px;
padding: 4px 12px; border-radius: var(--radius-xl);
font-size: 12px; font-weight: 600;
}
.status-pill.online { background: rgba(52,199,89,0.15); color: var(--ios-green); border: 1px solid rgba(52,199,89,0.3); }
.status-pill.offline { background: rgba(255,59,48,0.15); color: var(--ios-red); border: 1px solid rgba(255,59,48,0.3); }
.pulse { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: blink 1.5s ease-in-out infinite; }
.pulse { width: 7px; height: 7px; border-radius: var(--radius-full); background: currentColor; animation: blink 1.5s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.3} }
.content { padding: 20px 16px; max-width: 500px; margin: 0 auto; }
@@ -49,7 +49,7 @@
.card {
background: var(--grouped-background-secondary);
border-radius: 12px; overflow: hidden;
border-radius: var(--radius-md); overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card-row {
@@ -59,7 +59,7 @@
}
.card-row:last-child { border-bottom: none; }
.row-icon {
width: 36px; height: 36px; border-radius: 8px;
width: 36px; height: 36px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center; font-size: 18px;
flex-shrink: 0;
}
@@ -71,7 +71,7 @@
/* Atlas Viewer placeholder */
.atlas-viewer {
background: linear-gradient(135deg, #0a0a1a 0%, #0d1a2e 50%, #0a0a1a 100%);
border-radius: 12px; overflow: hidden;
border-radius: var(--radius-md); overflow: hidden;
aspect-ratio: 16/9; position: relative;
box-shadow: 0 4px 24px rgba(0,0,0,0.3);
}
@@ -90,7 +90,7 @@
.scan-points { position: absolute; inset: 0; }
.scan-point {
position: absolute; width: 8px; height: 8px;
border-radius: 50%; background: var(--ios-blue);
border-radius: var(--radius-full); background: var(--ios-blue);
box-shadow: 0 0 12px var(--ios-blue);
animation: scan-pulse 2s ease-in-out infinite;
}
@@ -112,7 +112,7 @@
}
.atlas-btn {
background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.2);
color: white; border-radius: 8px; padding: 6px 12px;
color: white; border-radius: var(--radius-md); padding: 6px 12px;
font-size: 13px; font-weight: 500; cursor: pointer;
backdrop-filter: blur(10px);
transition: background 0.2s;
@@ -125,7 +125,7 @@
}
.hud-chip {
background: rgba(242,242,247,0.5); border: 1px solid rgba(255,255,255,0.1);
color: rgba(255,255,255,0.8); border-radius: 6px;
color: rgba(255,255,255,0.8); border-radius: var(--radius-sm);
padding: 3px 8px; font-size: 11px; font-family: var(--font-mono);
backdrop-filter: blur(8px);
}
@@ -133,7 +133,7 @@
/* Coverage meter */
.coverage-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 16px;
border-radius: var(--radius-md); padding: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.coverage-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
@@ -141,10 +141,10 @@
.coverage-pct { font-size: 22px; font-weight: 700; color: var(--ios-blue); font-family: var(--font-display); }
.progress-track {
height: 8px; background: var(--fill-tertiary);
border-radius: 4px; overflow: hidden;
border-radius: var(--radius-sm); overflow: hidden;
}
.progress-fill {
height: 100%; border-radius: 4px;
height: 100%; border-radius: var(--radius-sm);
background: linear-gradient(90deg, var(--ios-blue) 0%, var(--ios-indigo) 100%);
transition: width 1s ease;
}
@@ -154,7 +154,7 @@
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.stat-card {
background: var(--grouped-background-secondary);
border-radius: 12px; padding: 14px 12px; text-align: center;
border-radius: var(--radius-md); padding: 14px 12px; text-align: center;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.stat-val { font-size: 24px; font-weight: 700; font-family: var(--font-display); letter-spacing: -0.5px; }
@@ -167,11 +167,11 @@
border-bottom: 0.5px solid var(--separator);
}
.region-row:last-child { border-bottom: none; }
.region-color { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.region-color { width: 10px; height: 10px; border-radius: var(--radius-full); flex-shrink: 0; }
.region-name { flex: 1; font-size: 15px; font-weight: 500; }
.region-count { font-size: 13px; color: var(--label-secondary); }
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: 3px; overflow: hidden; }
.region-bar { height: 100%; border-radius: 3px; }
.region-bar-wrap { width: 80px; height: 6px; background: var(--fill-tertiary); border-radius: var(--radius-sm); overflow: hidden; }
.region-bar { height: 100%; border-radius: var(--radius-sm); }
.footer { text-align: center; padding: 32px 0 16px; }
.footer-text { font-size: 12px; color: var(--label-tertiary); }
@@ -242,12 +242,12 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="hud-chip" id="hud-scans">Scans: —</div>
</div>
<div style="position:relative;z-index:2;text-align:center;">
<div class="atlas-label-main">🌐 3D Atlas</div>
<div class="atlas-label-main"> 3D Atlas</div>
<div class="atlas-label" id="atlas-status">Connecting to atlas service...</div>
</div>
<div class="atlas-controls">
<button class="atlas-btn" onclick="zoomIn()"></button>
<button class="atlas-btn" onclick="zoomOut()"></button>
<button class="atlas-btn" onclick="zoomIn()"></button>
<button class="atlas-btn" onclick="zoomOut()"></button>
<button class="atlas-btn" onclick="resetView()"></button>
</div>
</div>
@@ -287,7 +287,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Service Details</div>
<div class="card">
<div class="card-row">
<div class="row-icon" style="background:rgba(0,122,255,0.12);">🔌</div>
<div class="row-icon" style="background:rgba(0,122,255,0.12);"></div>
<div class="row-body">
<div class="row-label">Endpoint</div>
<div class="row-sub">Internal · port 7002</div>
@@ -295,7 +295,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value">localhost:7002</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(88,86,214,0.12);">📦</div>
<div class="row-icon" style="background:rgba(88,86,214,0.12);"></div>
<div class="row-body">
<div class="row-label">3D Engine</div>
<div class="row-sub">Point cloud · LIDAR</div>
@@ -303,7 +303,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value" id="engine-ver"></div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(52,199,89,0.12);">🗺</div>
<div class="row-icon" style="background:rgba(52,199,89,0.12);"></div>
<div class="row-body">
<div class="row-label">Map Format</div>
<div class="row-sub">Compressed tile format</div>
@@ -311,7 +311,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="row-value">3DGS v2</div>
</div>
<div class="card-row">
<div class="row-icon" style="background:rgba(255,149,0,0.12);"></div>
<div class="row-icon" style="background:rgba(255,149,0,0.12);"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M9 2L5 9H8L7 14L11 7H8L9 2Z" fill="#f59e0b"/></svg></div>
<div class="row-body">
<div class="row-label">Render API</div>
<div class="row-sub">WebGL 2 · GPU-accelerated</div>
@@ -401,12 +401,12 @@ async function refresh() {
if (health.online) {
pill.className = 'status-pill online';
statusText.textContent = 'Online';
document.getElementById('atlas-status').textContent = ' Atlas service connected · 3D render ready';
document.getElementById('atlas-status').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> Atlas service connected · 3D render ready';
document.getElementById('engine-ver').textContent = health.data?.version || 'v1.0';
} else {
pill.className = 'status-pill offline';
statusText.textContent = 'Offline';
document.getElementById('atlas-status').textContent = ' Atlas service unreachable';
document.getElementById('atlas-status').textContent = ' Atlas service unreachable';
}
const stats = await fetchAtlasStats();