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
+35 -35
View File
@@ -11,9 +11,9 @@
<title>LandveX — Mina objekt</title>
<link rel="stylesheet" >
<style>
/* ═══════════════════════════════════════════════════════
/*
LANDVEX KUNDPORTAL — iOS Native
═══════════════════════════════════════════════════════ */
*/
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-font-smoothing: antialiased; }
@@ -24,7 +24,7 @@ body {
min-height: 100vh;
}
/* ── LARGE TITLE NAV ── */
/* LARGE TITLE NAV */
.nav-bar {
position: sticky;
top: 0;
@@ -63,7 +63,7 @@ body {
.nav-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
border-radius: var(--radius-full);
background: linear-gradient(135deg, #007AFF, #5856D6);
display: flex;
align-items: center;
@@ -74,7 +74,7 @@ body {
cursor: pointer;
}
/* ── SEARCH BAR ── */
/* SEARCH BAR */
.search-bar {
padding: 0 16px 12px;
}
@@ -84,7 +84,7 @@ body {
align-items: center;
gap: 8px;
background: var(--fill-primary);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 0 12px;
height: 36px;
}
@@ -103,10 +103,10 @@ body {
.search-input::placeholder { color: var(--label-tertiary); }
/* ── CONTENT ── */
/* CONTENT */
.content { padding: 0 16px 100px; }
/* ── INSET GROUP ── */
/* INSET GROUP */
.inset-group {
margin-bottom: 28px;
}
@@ -123,18 +123,18 @@ body {
.inset-card {
background: var(--grouped-background-secondary);
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
border: 0.5px solid var(--separator);
}
/* ── HERO STATS ── */
/* HERO STATS */
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: var(--separator);
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 28px;
border: 0.5px solid var(--separator);
@@ -159,7 +159,7 @@ body {
margin-top: 2px;
}
/* ── OBJECT CARD ── */
/* OBJECT CARD */
.object-card {
display: flex;
align-items: center;
@@ -176,7 +176,7 @@ body {
.object-thumb {
width: 52px;
height: 52px;
border-radius: 12px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
@@ -213,7 +213,7 @@ body {
font-size: 13px;
font-weight: 700;
padding: 4px 10px;
border-radius: 20px;
border-radius: var(--radius-xl);
letter-spacing: -0.2px;
}
@@ -231,7 +231,7 @@ body {
color: var(--label-quaternary);
}
/* ── INSPECTION HISTORY ── */
/* INSPECTION HISTORY */
.history-item {
display: flex;
align-items: flex-start;
@@ -253,7 +253,7 @@ body {
.history-dot {
width: 10px;
height: 10px;
border-radius: 50%;
border-radius: var(--radius-full);
flex-shrink: 0;
}
@@ -286,7 +286,7 @@ body {
margin-top: 4px;
}
/* ── ACTION ROW ── */
/* ACTION ROW */
.action-row {
display: flex;
padding: 14px 16px;
@@ -305,14 +305,14 @@ body {
color: var(--ios-blue);
}
/* ── ORDER INSPECTION BUTTON ── */
/* ORDER INSPECTION BUTTON */
.order-btn {
width: 100%;
height: 54px;
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
font-family: var(--font-text);
@@ -327,11 +327,11 @@ body {
transform: scale(0.98);
}
/* ── NOTIFICATION BANNER ── */
/* NOTIFICATION BANNER */
.alert-banner {
background: rgba(255,59,48,0.1);
border: 0.5px solid rgba(255,59,48,0.3);
border-radius: 12px;
border-radius: var(--radius-md);
padding: 12px 14px;
margin-bottom: 16px;
display: flex;
@@ -353,7 +353,7 @@ body {
margin-top: 2px;
}
/* ── TAB BAR ── */
/* TAB BAR */
.tab-bar {
position: fixed;
bottom: 0;
@@ -430,7 +430,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="search-bar">
<div class="search-input-wrap">
<span class="search-icon">🔍</span>
<span class="search-icon"><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>
<input class="search-input" placeholder="Sök objekt..." type="search">
</div>
</div>
@@ -470,7 +470,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="inset-card">
<div class="object-card" onclick="showDetail('E4 Norrtull Avsnitt 12')">
<div class="object-thumb" style="background:rgba(255,149,0,0.15)">🛣️</div>
<div class="object-thumb" style="background:rgba(255,149,0,0.15)"></div>
<div class="object-body">
<div class="object-name">E4 Norrtull Avsnitt 12</div>
<div class="object-meta">Stockholm · Väg</div>
@@ -483,7 +483,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="object-card" onclick="showDetail('Gamla Stan Bro')">
<div class="object-thumb" style="background:rgba(52,199,89,0.15)">🌉</div>
<div class="object-thumb" style="background:rgba(52,199,89,0.15)"></div>
<div class="object-body">
<div class="object-name">Gamla Stan Bro</div>
<div class="object-meta">Stockholm · Bro</div>
@@ -496,7 +496,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="object-card" onclick="showDetail('Kungliga Slottet Tak')">
<div class="object-thumb" style="background:rgba(255,59,48,0.15)">🏛️</div>
<div class="object-thumb" style="background:rgba(255,59,48,0.15)"></div>
<div class="object-body">
<div class="object-name">Kungliga Slottet Tak</div>
<div class="object-meta">Stockholm · Byggnad</div>
@@ -509,7 +509,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="object-card" onclick="showDetail('Arlanda Terminal 5')">
<div class="object-thumb" style="background:rgba(0,122,255,0.15)">✈️</div>
<div class="object-thumb" style="background:rgba(0,122,255,0.15)"></div>
<div class="object-body">
<div class="object-name">Arlanda Terminal 5</div>
<div class="object-meta">Sigtuna · Byggnad</div>
@@ -522,7 +522,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="object-card" onclick="showDetail('Öresundsbron Sektion 3')">
<div class="object-thumb" style="background:rgba(255,149,0,0.15)">🌉</div>
<div class="object-thumb" style="background:rgba(255,149,0,0.15)"></div>
<div class="object-body">
<div class="object-name">Öresundsbron Sektion 3</div>
<div class="object-meta">Malmö · Bro</div>
@@ -539,7 +539,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- BESTÄLL INSPEKTION -->
<button class="order-btn" onclick="orderInspection()">
📋 Beställ ny inspektion
<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> Beställ ny inspektion
</button>
<!-- INSPECTION HISTORY -->
@@ -637,11 +637,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- TAB BAR -->
<nav class="tab-bar">
<div class="tab-item active">
<div class="tab-icon">🏠</div>
<div class="tab-icon"></div>
<div class="tab-label">Mina objekt</div>
</div>
<div class="tab-item">
<div class="tab-icon">🔍</div>
<div class="tab-icon"><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="tab-label">Inspektioner</div>
</div>
<div class="tab-item">
@@ -649,11 +649,11 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="tab-label">Rapporter</div>
</div>
<div class="tab-item">
<div class="tab-icon">🔔</div>
<div class="tab-icon"></div>
<div class="tab-label">Notiser</div>
</div>
<div class="tab-item">
<div class="tab-icon">👤</div>
<div class="tab-icon"></div>
<div class="tab-label">Konto</div>
</div>
</nav>
@@ -675,8 +675,8 @@ function orderInspection() {
const msg = `Beställ inspektion\n\nVälj objekt och datum i nästa steg.\nEn LandveX-inspektör kontaktar dig inom 24 timmar.\n\n[Bekräfta beställning]`;
if (confirm(msg)) {
const toast = document.createElement('div');
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-blue);color:white;padding:12px 20px;border-radius:20px;font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(0,122,255,0.4);white-space:nowrap;';
toast.textContent = ' Beställning skickad';
toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:var(--ios-blue);color:white;padding:12px 20px;border-radius: var(--radius-xl);font-size:15px;font-weight:600;z-index:500;box-shadow:0 4px 16px rgba(0,122,255,0.4);white-space:nowrap;';
toast.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> Beställning skickad';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
}