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
+147
View File
@@ -0,0 +1,147 @@
// Academy Auth Integration
// Kopplar academy-progress till quiXzoom inloggningssystem
class AcademyAuth {
constructor() {
this.apiUrl = 'https://api.quixzoom.com';
this.token = this.getToken();
this.user = this.getUser();
}
getToken() {
// Kolla flera möjliga token-lagringar
return localStorage.getItem('qz_token') ||
localStorage.getItem('auth_token') ||
sessionStorage.getItem('qz_token') ||
this.getCookie('qz_session');
}
getUser() {
try {
return JSON.parse(localStorage.getItem('qz_user') || '{}');
} catch {
return {};
}
}
getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
isLoggedIn() {
return !!this.token;
}
// Hämta progress från backend
async fetchProgress() {
if (!this.isLoggedIn()) return null;
try {
const response = await fetch(`${this.apiUrl}/academy/progress`, {
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
// Spara till localStorage som backup
localStorage.setItem('academyProgress', JSON.stringify(data.completedLessons || []));
return data;
}
} catch (error) {
console.warn('Kunde inte hämta progress från backend:', error);
}
return null;
}
// Spara progress till backend
async saveProgress(lessonId) {
if (!this.isLoggedIn()) {
// Spara lokalt tills användaren loggar in
this.saveLocalProgress(lessonId);
return false;
}
try {
const response = await fetch(`${this.apiUrl}/academy/progress`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ lessonId, completedAt: new Date().toISOString() })
});
if (response.ok) {
// Uppdatera även localStorage
this.saveLocalProgress(lessonId);
return true;
}
} catch (error) {
console.warn('Kunde inte spara progress till backend:', error);
// Fallback till localStorage
this.saveLocalProgress(lessonId);
}
return false;
}
saveLocalProgress(lessonId) {
const progress = JSON.parse(localStorage.getItem('academyProgress') || '[]');
if (!progress.includes(lessonId)) {
progress.push(lessonId);
localStorage.setItem('academyProgress', JSON.stringify(progress));
}
}
// Visa inloggnings-prompt
showLoginPrompt() {
const prompt = document.createElement('div');
prompt.id = 'academy-login-prompt';
prompt.innerHTML = `
<div style="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);z-index:10000;display:flex;align-items:center;justify-content:center;padding:20px">
<div style="background:white;border-radius:20px;padding:40px;max-width:400px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3)">
<div style="font-size:48px;margin-bottom:16px">🔐</div>
<h2 style="font-size:24px;font-weight:800;margin-bottom:12px">Logga in för att spara din progress</h2>
<p style="color:#6E6E73;margin-bottom:24px">Dina lektioner och certifiering sparas till ditt quiXzoom-konto så du kan fortsätta på vilken enhet som helst.</p>
<div style="display:flex;gap:12px;flex-direction:column">
<a href="/login?redirect=/academy/" style="background:#0066FF;color:white;padding:16px;border-radius:12px;text-decoration:none;font-weight:700;font-size:16px">Logga in</a>
<button onclick="document.getElementById('academy-login-prompt').remove()" style="background:#F5F5F7;color:#0A0A0A;padding:16px;border-radius:12px;border:none;font-weight:600;font-size:16px;cursor:pointer">Fortsätt utan inloggning</button>
</div>
</div>
</div>
`;
document.body.appendChild(prompt);
}
// Hämta certifiering
async getCertificate() {
if (!this.isLoggedIn()) return null;
try {
const response = await fetch(`${this.apiUrl}/academy/certificate`, {
headers: {
'Authorization': `Bearer ${this.token}`
}
});
if (response.ok) {
return await response.json();
}
} catch (error) {
console.warn('Kunde inte hämta certifikat:', error);
}
return null;
}
}
// Initiera
const academyAuth = new AcademyAuth();
// Exportera för global användning
window.academyAuth = academyAuth;