262 lines
8.1 KiB
JavaScript
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;
|