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
+47 -47
View File
@@ -50,7 +50,7 @@ body {
padding-bottom: calc(var(--safe-bottom) + 20px);
}
/* ── Navigation Bar ── */
/* Navigation Bar */
.navbar {
position: fixed;
top: 0; left: 0; right: 0;
@@ -72,7 +72,7 @@ body {
.navbar-icon {
width: 30px; height: 30px;
background: linear-gradient(135deg, #007AFF, #5856D6);
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 16px;
flex-shrink: 0;
@@ -92,18 +92,18 @@ body {
color: var(--ios-blue);
background: rgba(0,122,255,.1);
padding: 3px 8px;
border-radius: 20px;
border-radius: var(--radius-xl);
}
/* ── Content ── */
/* Content */
.content { padding: 0 16px; }
/* ── Alert Banner ── */
/* Alert Banner */
.deadline-alert {
margin: 20px 0 0;
background: linear-gradient(135deg, rgba(255,59,48,.08), rgba(255,149,0,.08));
border: 1px solid rgba(255,59,48,.2);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
display: flex;
align-items: flex-start;
@@ -113,7 +113,7 @@ body {
.alert-icon {
width: 36px; height: 36px;
background: rgba(255,59,48,.12);
border-radius: 10px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 18px;
flex-shrink: 0;
@@ -148,7 +148,7 @@ body {
letter-spacing: 0.5px;
}
/* ── Section Header ── */
/* Section Header */
.section-header {
font-size: 13px;
font-weight: 600;
@@ -158,10 +158,10 @@ body {
padding: 24px 4px 8px;
}
/* ── Cards ── */
/* Cards */
.card {
background: var(--grouped-bg2);
border-radius: 16px;
border-radius: var(--radius-lg);
overflow: hidden;
margin-bottom: 12px;
}
@@ -183,7 +183,7 @@ body {
line-height: 1.4;
}
/* ── Info Row ── */
/* Info Row */
.info-row {
display: flex;
align-items: center;
@@ -196,7 +196,7 @@ body {
.info-icon {
width: 32px; height: 32px;
border-radius: 8px;
border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
font-size: 15px;
flex-shrink: 0;
@@ -217,7 +217,7 @@ body {
text-align: right;
}
/* ── Stats Row ── */
/* Stats Row */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
@@ -248,7 +248,7 @@ body {
letter-spacing: 0.3px;
}
/* ── Zoomer Table ── */
/* Zoomer Table */
.zoomer-row {
display: flex;
align-items: center;
@@ -264,7 +264,7 @@ body {
.zoomer-avatar {
width: 40px; height: 40px;
border-radius: 50%;
border-radius: var(--radius-full);
display: flex; align-items: center; justify-content: center;
font-size: 16px;
font-weight: 700;
@@ -296,7 +296,7 @@ body {
font-size: 11px;
font-weight: 600;
padding: 3px 8px;
border-radius: 20px;
border-radius: var(--radius-xl);
margin-top: 4px;
}
@@ -315,10 +315,10 @@ body {
color: var(--ios-orange);
}
/* ── Info Section ── */
/* Info Section */
.info-card {
background: var(--grouped-bg2);
border-radius: 16px;
border-radius: var(--radius-lg);
padding: 16px;
margin-bottom: 12px;
}
@@ -344,7 +344,7 @@ body {
font-weight: 600;
}
/* ── Export Button ── */
/* Export Button */
.export-section {
margin: 8px 0 24px;
}
@@ -359,7 +359,7 @@ body {
background: var(--ios-blue);
color: white;
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 17px;
font-weight: 600;
letter-spacing: -0.41px;
@@ -380,7 +380,7 @@ body {
background: var(--ios-gray6);
color: var(--ios-blue);
border: none;
border-radius: 14px;
border-radius: var(--radius-lg);
font-size: 15px;
font-weight: 600;
letter-spacing: -0.23px;
@@ -391,7 +391,7 @@ body {
.btn-secondary:active { opacity: 0.7; }
/* ── Period Card ── */
/* Period Card */
.period-row {
display: flex;
align-items: center;
@@ -405,7 +405,7 @@ body {
.period-label { font-size: 15px; letter-spacing: -0.23px; }
.period-value { font-size: 15px; font-weight: 500; color: var(--label-secondary); }
/* ── Divider Text ── */
/* Divider Text */
.divider-text {
text-align: center;
font-size: 12px;
@@ -414,7 +414,7 @@ body {
line-height: 1.4;
}
/* ── Toast ── */
/* Toast */
.toast {
position: fixed;
bottom: calc(var(--safe-bottom) + 24px);
@@ -437,18 +437,18 @@ body {
.toast.show { opacity: 1; }
/* ── Progress Bar ── */
/* Progress Bar */
.progress-track {
height: 4px;
background: var(--ios-gray5);
border-radius: 2px;
border-radius: var(--radius-sm);
margin: 12px 16px 14px;
overflow: hidden;
}
.progress-fill {
height: 100%;
border-radius: 2px;
border-radius: var(--radius-sm);
background: var(--ios-blue);
transition: width 0.5s ease;
}
@@ -485,7 +485,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- Navigation Bar -->
<div class="navbar">
<div class="navbar-icon">🧾</div>
<div class="navbar-icon"></div>
<div class="navbar-title">DAC7 Rapportering</div>
<div class="navbar-badge">2026</div>
</div>
@@ -508,7 +508,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<!-- What is DAC7 -->
<div class="section-header">Om DAC7</div>
<div class="info-card">
<div class="info-card-title">📋 Vad är DAC7?</div>
<div class="info-card-title"><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> Vad är DAC7?</div>
<div class="info-card-text">
<strong>EU-direktiv 2021/514 (DAC7)</strong> kräver att digitala plattformar rapporterar säljares (zoomers) intäkter till skattemyndigheterna. quiXzoom är en <strong>rapporteringsskyldig plattformsoperatör</strong> och måste varje år lämna uppgifter om zoomers som:
<br><br>
@@ -520,7 +520,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
</div>
<div class="info-card">
<div class="info-card-title">🏛️ Vår skyldighet</div>
<div class="info-card-title"> Vår skyldighet</div>
<div class="info-card-text">
quiXzoom samlar in <strong>TIN (skatteregistreringsnummer)</strong> och personuppgifter från alla aktiva zoomers. Vi verifierar uppgifterna och rapporterar automatiskt till Skatteverket via deras API-integration. Zoomers informeras om rapporteringen i tjänstevillkoren.
</div>
@@ -597,7 +597,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Alicia Bergström</div>
<div class="zoomer-tin">TIN: 8804127654</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">12 600 SEK</div>
@@ -611,7 +611,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Mikael Thorsson</div>
<div class="zoomer-tin">TIN: 9103284512</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">9 750 SEK</div>
@@ -625,7 +625,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Fatima Okonkwo</div>
<div class="zoomer-tin">TIN: EJ REGISTRERAT</div>
<span class="status-badge status-missing"> TIN saknas</span>
<span class="status-badge status-missing"><svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M4 4L12 12M12 4L4 12" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> TIN saknas</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">7 200 SEK</div>
@@ -653,7 +653,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="zoomer-info">
<div class="zoomer-name">Sofia Wennberg</div>
<div class="zoomer-tin">TIN: 0002157890</div>
<span class="status-badge status-verified"> TIN verifierad</span>
<span class="status-badge status-verified"><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> TIN verifierad</span>
</div>
<div class="zoomer-right">
<div class="zoomer-amount">2 400 SEK</div>
@@ -671,18 +671,18 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="section-header">Exportera rapport</div>
<div class="export-section">
<button class="btn-primary" onclick="exportXML()">
<span>📤</span>
<span></span>
Exportera XML till Skatteverket
</button>
<button class="btn-secondary" onclick="downloadPreview()">
<span>👁️</span>
<span></span>
Förhandsgranska XML
</button>
</div>
<!-- Footer note -->
<div class="info-card" style="margin-bottom: 24px;">
<div class="info-card-title">⚠️ Viktigt att notera</div>
<div class="info-card-title"><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> Viktigt att notera</div>
<div class="info-card-text">
Zoomers med <strong>saknat TIN</strong> måste kontaktas och komplettera sina uppgifter <strong>innan export</strong>. Rapporten kan inte inkludera saknade skattenummer.
<br><br>
@@ -696,7 +696,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
<div class="toast" id="toast"></div>
<script>
// ── Days Until Deadline ──
// Days Until Deadline
(function() {
const deadline = new Date('2027-01-31T23:59:59');
const now = new Date();
@@ -705,7 +705,7 @@ body { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
if (el) el.textContent = diff > 0 ? diff : '0';
})();
// ── Toast Helper ──
// Toast Helper
function showToast(msg, duration = 2500) {
const t = document.getElementById('toast');
t.textContent = msg;
@@ -713,17 +713,17 @@ function showToast(msg, duration = 2500) {
setTimeout(() => t.classList.remove('show'), duration);
}
// ── Zoomer Detail ──
// Zoomer Detail
function showZoomerDetail(name, tin, amount, status) {
const statusMap = {
verified: ' TIN verifierad',
missing: ' TIN saknas',
verified: '<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> TIN verifierad',
missing: '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="#ef4444" stroke-width="2"/><path d="M7 7L13 13M13 7L7 13" stroke="#ef4444" stroke-width="2" stroke-linecap="round"/></svg> TIN saknas',
pending: '⏳ Under utredning'
};
showToast(`${name} · ${parseInt(amount).toLocaleString('sv-SE')} SEK · ${statusMap[status]}`);
}
// ── Export XML ──
// Export XML
function exportXML() {
const btn = document.querySelector('.btn-primary');
const orig = btn.innerHTML;
@@ -741,13 +741,13 @@ function exportXML() {
URL.revokeObjectURL(url);
btn.innerHTML = orig;
btn.style.opacity = '';
showToast(' XML-fil exporterad!', 3000);
showToast('<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> XML-fil exporterad!', 3000);
}, 1200);
}
// ── Preview XML ──
// Preview XML
function downloadPreview() {
showToast('📄 Öppnar förhandsgranskning...');
showToast(' Öppnar förhandsgranskning...');
setTimeout(() => {
const xml = generateDAC7XML();
const win = window.open('', '_blank');
@@ -761,7 +761,7 @@ function escapeHtml(str) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ── DAC7 XML Generator ──
// DAC7 XML Generator
function generateDAC7XML() {
const now = new Date().toISOString();
const zoomers = [