moxie-site/js/auth.js
2026-05-08 00:14:58 +00:00

262 lines
8.1 KiB
JavaScript

/**
* Moxiegen Authentication Module
* Handles Auth0 authentication with backend token exchange
*/
class MoxieAuth {
constructor() {
this.isAuthenticated = false;
this.user = null;
this.token = null;
this.expiresAt = null;
this.isInitialized = false;
}
/**
* Initialize - check for existing session or handle callback
*/
async init() {
if (this.isInitialized) return;
try {
// Check for tokens in URL (coming back from token exchange)
const urlParams = new URLSearchParams(window.location.search);
const accessToken = urlParams.get('access_token');
const idToken = urlParams.get('id_token');
const expiresIn = urlParams.get('expires_in');
const code = urlParams.get('code');
const error = urlParams.get('error');
if (error) {
console.error('Auth error:', error, urlParams.get('error_description'));
this.clearTokens();
window.history.replaceState({}, document.title, window.location.pathname);
throw new Error(urlParams.get('error_description') || error);
}
// If we have a code but no token, exchange it via backend
if (code && !accessToken) {
console.log('Exchanging authorization code for tokens...');
await this.exchangeCode(code);
return;
}
if (accessToken) {
// Store tokens from URL
this.token = accessToken;
this.expiresAt = Date.now() + (parseInt(expiresIn) || 3600) * 1000;
if (idToken) {
this.user = this.decodeJwt(idToken);
localStorage.setItem('id_token', idToken);
}
localStorage.setItem('access_token', accessToken);
localStorage.setItem('expires_at', this.expiresAt.toString());
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname);
this.isAuthenticated = true;
this.isInitialized = true;
return;
}
// Check for stored tokens
const storedToken = localStorage.getItem('access_token');
const storedExpiresAt = localStorage.getItem('expires_at');
const storedIdToken = localStorage.getItem('id_token');
if (storedToken && storedExpiresAt) {
if (Date.now() < parseInt(storedExpiresAt)) {
this.token = storedToken;
this.expiresAt = parseInt(storedExpiresAt);
if (storedIdToken) {
this.user = this.decodeJwt(storedIdToken);
}
this.isAuthenticated = true;
} else {
// Token expired - clear it
this.clearTokens();
}
}
this.isInitialized = true;
console.log('Auth initialized. Authenticated:', this.isAuthenticated);
} catch (error) {
console.error('Failed to initialize auth:', error);
this.isInitialized = true;
throw error;
}
}
/**
* Exchange authorization code for tokens via backend
*/
async exchangeCode(code) {
try {
const response = await fetch('/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
redirect_uri: window.location.origin + '/dashboard.html'
})
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message || 'Token exchange failed');
}
const { access_token, id_token, expires_in } = data.data;
// Store tokens
this.token = access_token;
this.expiresAt = Date.now() + (expires_in || 3600) * 1000;
if (id_token) {
this.user = this.decodeJwt(id_token);
localStorage.setItem('id_token', id_token);
}
localStorage.setItem('access_token', access_token);
localStorage.setItem('expires_at', this.expiresAt.toString());
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname);
this.isAuthenticated = true;
this.isInitialized = true;
console.log('Token exchange successful');
} catch (error) {
console.error('Token exchange error:', error);
// Redirect to login on failure
this.clearTokens();
window.history.replaceState({}, document.title, window.location.pathname);
throw error;
}
}
/**
* Decode JWT (just the payload, no verification)
*/
decodeJwt(token) {
try {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(c => {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
} catch (e) {
console.error('Failed to decode JWT:', e);
return null;
}
}
/**
* Login - redirect to Auth0
*/
async login(returnTo = null) {
const redirectUri = returnTo || window.location.origin + '/dashboard.html';
// Build Auth0 authorize URL
let authUrl = `https://${CONFIG.auth0.domain}/authorize?` +
`response_type=code&` +
`client_id=${CONFIG.auth0.clientId}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`scope=openid%20profile%20email`;
// Only add audience if it's defined and not empty
if (CONFIG.auth0.audience && CONFIG.auth0.audience.trim() !== '') {
authUrl += `&audience=${encodeURIComponent(CONFIG.auth0.audience)}`;
}
window.location.href = authUrl;
}
/**
* Logout
*/
async logout() {
this.clearTokens();
const returnTo = encodeURIComponent(window.location.origin);
const logoutUrl = `https://${CONFIG.auth0.domain}/v2/logout?client_id=${CONFIG.auth0.clientId}&returnTo=${returnTo}`;
window.location.href = logoutUrl;
}
/**
* Clear stored tokens
*/
clearTokens() {
localStorage.removeItem('access_token');
localStorage.removeItem('id_token');
localStorage.removeItem('expires_at');
this.token = null;
this.user = null;
this.isAuthenticated = false;
this.expiresAt = null;
}
/**
* Get access token
*/
async getToken() {
if (!this.isInitialized) {
await this.init();
}
// Check if token is expired
if (this.expiresAt && Date.now() >= this.expiresAt) {
this.clearTokens();
return null;
}
return this.token;
}
/**
* Get current user
*/
async getUser() {
if (!this.isInitialized) {
await this.init();
}
return this.user;
}
/**
* Check if user is authenticated
*/
async checkAuth() {
if (!this.isInitialized) {
await this.init();
}
return this.isAuthenticated;
}
/**
* Check if user has specific role
*/
hasRole(role) {
if (!this.user) return false;
const roles = this.user['https://moxiegen.client.guacamolebox.net/roles'] ||
this.user.roles ||
[];
return Array.isArray(roles) && roles.includes(role);
}
}
// Create singleton instance
const moxieAuth = new MoxieAuth();
// Export for use in other modules
window.moxieAuth = moxieAuth;