dev-api: add developer portal, OpenAPI spec, and Flatenbadet case study
- New /developers/ page with API docs, SDKs, pricing, use cases - OpenAPI 3.0 spec for Orders, Missions, Photos, Analytics - Case study: Glasskiosken i Flatenbadet — complete ROI analysis - Updated /order/ with recurring missions and frequency dropdown
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* quiXzoom SSO Client v2.0
|
||||
* Universal cross-domain silent login
|
||||
*
|
||||
* Usage:
|
||||
* <script src="https://auth.quixzoom.com/sso-client-v2.js"></script>
|
||||
* <script>
|
||||
* QZAuth.init({
|
||||
* ssoUrl: 'https://auth.quixzoom.com',
|
||||
* onLogin: (user) => console.log('Logged in:', user),
|
||||
* onLogout: () => console.log('Logged out'),
|
||||
* });
|
||||
* </script>
|
||||
*/
|
||||
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
ssoUrl: 'https://auth.quixzoom.com',
|
||||
checkInterval: 60000, // Check auth every 60 seconds
|
||||
silentTimeout: 5000,
|
||||
};
|
||||
|
||||
class QZAuthClient {
|
||||
constructor() {
|
||||
this.config = { ...DEFAULT_CONFIG };
|
||||
this.user = null;
|
||||
this.isAuthenticated = false;
|
||||
this.callbacks = {
|
||||
onLogin: null,
|
||||
onLogout: null,
|
||||
onError: null,
|
||||
};
|
||||
this.checkIntervalId = null;
|
||||
}
|
||||
|
||||
init(config = {}) {
|
||||
this.config = { ...this.config, ...config };
|
||||
|
||||
if (config.onLogin) this.callbacks.onLogin = config.onLogin;
|
||||
if (config.onLogout) this.callbacks.onLogout = config.onLogout;
|
||||
if (config.onError) this.callbacks.onError = config.onError;
|
||||
|
||||
// Check auth immediately
|
||||
this.checkAuth();
|
||||
|
||||
// Set up periodic checks
|
||||
this.checkIntervalId = setInterval(() => {
|
||||
this.checkAuth();
|
||||
}, this.config.checkInterval);
|
||||
|
||||
// Listen for storage events (login from other tabs)
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'qz_auth_event') {
|
||||
const event = JSON.parse(e.newValue || '{}');
|
||||
if (event.type === 'login') {
|
||||
this.checkAuth();
|
||||
} else if (event.type === 'logout') {
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for messages from SSO iframe
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.origin !== this.config.ssoUrl) return;
|
||||
|
||||
if (e.data.type === 'qz_auth_silent_response') {
|
||||
this.handleSilentResponse(e.data);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[QZAuth] Initialized');
|
||||
}
|
||||
|
||||
async checkAuth() {
|
||||
try {
|
||||
// Try silent login first (uses refresh token cookie)
|
||||
const response = await fetch(`${this.config.ssoUrl}/auth/silent`, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.authenticated) {
|
||||
this.setUser(data.user);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Silent login failed, check if we have a token in URL (OAuth redirect)
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const accessToken = urlParams.get('access_token');
|
||||
|
||||
if (accessToken) {
|
||||
// Verify token with SSO
|
||||
const verifyResponse = await fetch(`${this.config.ssoUrl}/auth/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (verifyResponse.ok) {
|
||||
const user = await verifyResponse.json();
|
||||
this.setUser(user);
|
||||
// Clean URL
|
||||
window.history.replaceState({}, document.title, window.location.pathname);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Not authenticated
|
||||
this.clearUser();
|
||||
} catch (error) {
|
||||
console.error('[QZAuth] Auth check failed:', error);
|
||||
if (this.callbacks.onError) {
|
||||
this.callbacks.onError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSilentResponse(data) {
|
||||
if (data.authenticated) {
|
||||
this.setUser(data.user);
|
||||
} else {
|
||||
this.clearUser();
|
||||
}
|
||||
}
|
||||
|
||||
setUser(user) {
|
||||
const wasAuthenticated = this.isAuthenticated;
|
||||
this.user = user;
|
||||
this.isAuthenticated = true;
|
||||
|
||||
// Store in localStorage for other tabs
|
||||
localStorage.setItem('qz_user', JSON.stringify(user));
|
||||
localStorage.setItem('qz_auth_event', JSON.stringify({
|
||||
type: 'login',
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
|
||||
// Update UI
|
||||
this.updateUI();
|
||||
|
||||
// Trigger callback if newly logged in
|
||||
if (!wasAuthenticated && this.callbacks.onLogin) {
|
||||
this.callbacks.onLogin(user);
|
||||
}
|
||||
}
|
||||
|
||||
clearUser() {
|
||||
const wasAuthenticated = this.isAuthenticated;
|
||||
this.user = null;
|
||||
this.isAuthenticated = false;
|
||||
|
||||
localStorage.removeItem('qz_user');
|
||||
localStorage.setItem('qz_auth_event', JSON.stringify({
|
||||
type: 'logout',
|
||||
timestamp: Date.now(),
|
||||
}));
|
||||
|
||||
this.updateUI();
|
||||
|
||||
if (wasAuthenticated && this.callbacks.onLogout) {
|
||||
this.callbacks.onLogout();
|
||||
}
|
||||
}
|
||||
|
||||
updateUI() {
|
||||
// Update all elements with data-qz-auth attribute
|
||||
document.querySelectorAll('[data-qz-auth]').forEach(el => {
|
||||
const action = el.getAttribute('data-qz-auth');
|
||||
|
||||
if (action === 'show-when-authenticated') {
|
||||
el.style.display = this.isAuthenticated ? '' : 'none';
|
||||
} else if (action === 'show-when-guest') {
|
||||
el.style.display = !this.isAuthenticated ? '' : 'none';
|
||||
} else if (action === 'user-name') {
|
||||
el.textContent = this.user?.name || '';
|
||||
} else if (action === 'user-email') {
|
||||
el.textContent = this.user?.email || '';
|
||||
} else if (action === 'user-avatar') {
|
||||
el.src = this.user?.avatar || '/default-avatar.png';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await fetch(`${this.config.ssoUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.setUser(data.user);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('[QZAuth] Login failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
await fetch(`${this.config.ssoUrl}/auth/logout`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('[QZAuth] Logout error:', error);
|
||||
} finally {
|
||||
this.clearUser();
|
||||
}
|
||||
}
|
||||
|
||||
getUser() {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
isLoggedIn() {
|
||||
return this.isAuthenticated;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.checkIntervalId) {
|
||||
clearInterval(this.checkIntervalId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
global.QZAuth = new QZAuthClient();
|
||||
|
||||
})(window);
|
||||
Reference in New Issue
Block a user