import axios, { AxiosInstance } from 'axios';
import { AuthCredentials, AuthToken, AuthCredentialsSchema, AuthenticationResponse } from './types.js';
export class UmbrellaAuth {
private baseURL: string;
private axiosInstance: AxiosInstance;
private token: string | null = null;
private userApiKey: string | null = null;
private tokenExpiry: number = 0;
constructor(baseURL: string) {
this.baseURL = baseURL;
this.axiosInstance = axios.create({
baseURL: this.baseURL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
}
async authenticate(credentials: AuthCredentials): Promise<AuthToken> {
// Validate credentials
const validatedCredentials = AuthCredentialsSchema.parse(credentials);
try {
// Try the authentication endpoint that matches the frontend-app-server
const response = await this.axiosInstance.post('/authentication/token/generate', {
username: validatedCredentials.username,
password: validatedCredentials.password
});
if (response.status === 200 && response.data.Authorization && response.data.apikey) {
this.token = response.data.Authorization;
// CRITICAL FIX: Build proper API key format by getting user's account data
const tempApiKey = response.data.apikey; // This is userKey:-1
const userKey = tempApiKey.split(':')[0];
// Get user's actual account information
const properApiKey = await this.buildProperApiKey(userKey, response.data.Authorization);
this.userApiKey = properApiKey;
// Set token expiry to 1 hour from now (typical JWT expiry)
this.tokenExpiry = Date.now() + (60 * 60 * 1000);
return {
Authorization: response.data.Authorization
};
} else {
throw new Error('Invalid response from authentication endpoint');
}
} catch (error: any) {
if (error.response) {
// Server responded with error status
const status = error.response.status;
const message = error.response.data?.message || error.response.statusText || 'Authentication failed';
if (status === 401) {
throw new Error(`Authentication failed: Invalid credentials. ${message}`);
} else if (status === 429) {
throw new Error(`Authentication failed: Too many requests. Please wait and try again. ${message}`);
} else {
throw new Error(`Authentication failed: Server error (${status}). ${message}`);
}
} else if (error.request) {
// No response received
throw new Error('Authentication failed: Unable to connect to Umbrella Cost API. Please check your network connection.');
} else {
// Request setup error
throw new Error(`Authentication failed: ${error.message}`);
}
}
}
/**
* Build proper API key format by getting user's account data
* Format: userKey:accountKey:divisionId
*/
private async buildProperApiKey(userKey: string, jwtToken: string): Promise<string> {
try {
// Get user's accounts using the temporary API key with JWT
const tempApiKey = `${userKey}:-1`; // Use temp key to get accounts
const accountsResponse = await this.axiosInstance.get('/user-management/accounts', {
headers: {
'Authorization': jwtToken,
'apikey': tempApiKey,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
if (accountsResponse.status === 200 && accountsResponse.data && accountsResponse.data.length > 0) {
// Use the first account (primary account)
const firstAccount = accountsResponse.data[0];
const accountKey = firstAccount.accountKey || firstAccount.accountId;
const divisionId = 0; // Default division as seen in frontend code
const properApiKey = `${userKey}:${accountKey}:${divisionId}`;
console.error(`[AUTH] Built proper API key: ${properApiKey}`);
return properApiKey;
} else {
console.error('[AUTH] No accounts found, using temporary API key');
return tempApiKey; // Fallback to temp key
}
} catch (error: any) {
console.error(`[AUTH] Failed to get account data: ${error.message}`);
return `${userKey}:-1`; // Fallback to original format
}
}
/**
* Get current authentication headers for API requests
*/
getAuthHeaders(): AuthToken {
if (!this.token || !this.userApiKey) {
throw new Error('Not authenticated. Call authenticate() first.');
}
if (Date.now() > this.tokenExpiry) {
throw new Error('Token expired. Please re-authenticate.');
}
return {
Authorization: this.token // Raw JWT token, not Bearer
};
}
/**
* Check if the current token is still valid
*/
isAuthenticated(): boolean {
return !!(this.token && this.userApiKey && Date.now() < this.tokenExpiry);
}
/**
* Clear the stored authentication data
*/
clearAuth(): void {
this.token = null;
this.userApiKey = null;
this.tokenExpiry = 0;
}
async validateToken(token: string): Promise<boolean> {
try {
// Try to make a simple request with the token to validate it
const response = await this.axiosInstance.get('/health', {
headers: {
'Authorization': token // Raw JWT token
}
});
return response.status === 200;
} catch {
return false;
}
}
}