ideas-generator/admin/src/services/api.js
DJP 013f57fe60 Implement hybrid Azure AD SSO + Password authentication system
 Backend Implementation:
- Add Azure AD JWT token validation middleware
- Create hybrid authentication system supporting both Azure AD and password auth
- Implement auto-provisioning for new Azure AD users
- Add admin controls to toggle password authentication
- Update all API routes to use hybrid authentication
- Add database fields for authentication (password, lastLoginAt)
- Create comprehensive auth routes with validation endpoints

 Frontend Implementation:
- Install and configure Azure MSAL browser library
- Create Azure AD authentication service with popup/redirect support
- Build hybrid authentication service managing both auth methods
- Update Login.vue with modern dual-authentication UI
- Implement dynamic password auth toggle based on admin settings
- Update App.vue for proper session management and validation
- Modify API service to handle both token types

 Security Features:
- Azure AD tenant validation (Oliver Agency)
- Role-based access control with auto-admin assignment
- JWT token validation for both auth methods
- Automatic user provisioning with proper defaults
- Session validation and automatic logout on token expiry

 Admin Features:
- Toggle password authentication on/off
- Manage users from both authentication methods
- Full role and agent access control
- Azure AD user auto-provisioning as regular users

 Configuration:
- Azure AD: Tenant e519c2e6-bc6d-4fdf-8d9c-923c2f002385
- Client ID: 9079054c-9620-4757-a256-23413042f1ef
- Development redirect URI support
- Fallback password authentication for testing

🔧 Technical Stack:
- Azure MSAL Browser & Node libraries
- JWT token validation and hybrid middleware
- Database schema updates with migrations
- Vue.js integration with MSAL
- Express.js hybrid authentication routes

🚀 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-09 16:14:02 -04:00

259 lines
No EOL
6.9 KiB
JavaScript

import axios from 'axios'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api'
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
})
// Add authentication interceptor
api.interceptors.request.use(
async (config) => {
// Try to get token from either auth method
const authToken = localStorage.getItem('authToken');
const azureToken = localStorage.getItem('azureAuthToken');
const token = authToken || azureToken;
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => Promise.reject(error)
)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired or invalid, clear all tokens and redirect to login
localStorage.removeItem('authToken');
localStorage.removeItem('azureAuthToken');
localStorage.removeItem('currentUser');
window.location.href = '/login';
}
console.error('API Error:', error.response?.data || error.message)
return Promise.reject(error)
}
)
export const agentsAPI = {
async getAll(admin = false) {
const params = admin ? { admin: 'true' } : {};
// Add userId for permission filtering (unless admin request)
if (!admin) {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
const user = JSON.parse(currentUser);
params.userId = user.id;
}
}
const response = await api.get('/assistants', { params })
return response.data
},
async getByKey(key) {
const response = await api.get(`/assistants/${key}`)
return response.data
},
async update(key, data) {
const response = await api.put(`/assistants/${key}`, data)
return response.data
},
async toggleStatus(key) {
const response = await api.patch(`/assistants/${key}/toggle-status`)
return response.data
},
async create(data) {
const response = await api.post('/assistants', data)
return response.data
},
async delete(key) {
const response = await api.delete(`/assistants/${key}`)
return response.data
}
}
// Keep old name for backward compatibility during migration
export const assistantsAPI = agentsAPI
export const chatAPI = {
async sendMessage(data, files = []) {
if (files && files.length > 0) {
const formData = new FormData()
// Add text data
Object.keys(data).forEach(key => {
if (key !== 'files') {
formData.append(key, typeof data[key] === 'object' ? JSON.stringify(data[key]) : data[key])
}
})
// Add files
files.forEach(file => {
formData.append('files', file)
})
const response = await api.post('/chat/completions', formData, {
headers: {
'Content-Type': 'multipart/form-data',
}
})
return response.data
} else {
const response = await api.post('/chat/completions', data)
return response.data
}
},
async sendStreamingMessage(data, onChunk, files = []) {
let requestBody
let headers = {}
if (files && files.length > 0) {
const formData = new FormData()
// Add text data
Object.keys(data).forEach(key => {
if (key !== 'files') {
formData.append(key, typeof data[key] === 'object' ? JSON.stringify(data[key]) : data[key])
}
})
formData.append('stream', 'true')
// Add files
files.forEach(file => {
formData.append('files', file)
})
requestBody = formData
} else {
headers['Content-Type'] = 'application/json'
requestBody = JSON.stringify({ ...data, stream: true })
}
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
method: 'POST',
headers,
body: requestBody
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const reader = response.body.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value)
const lines = chunk.split('\n')
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6))
if (onChunk) onChunk(data)
} catch (e) {
console.warn('Failed to parse SSE data:', line)
}
}
}
}
} finally {
reader.releaseLock()
}
},
async getConversationMessages(conversationId, limit = 50, offset = 0) {
const response = await api.get(`/chat/conversations/${conversationId}/messages`, {
params: { limit, offset }
})
return response.data
},
async getConversations(userId, limit = 20, offset = 0) {
const response = await api.get('/chat/conversations', {
params: { userId, limit, offset }
})
return response.data
}
}
export const analyticsAPI = {
async getUsageData(filters = {}) {
const params = {};
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
if (filters.userId) params.userId = filters.userId;
if (filters.agentKey) params.agentKey = filters.agentKey;
const response = await api.get('/analytics/usage', { params });
return response.data;
},
async getUsageStats(filters = {}) {
const params = {};
if (filters.startDate) params.startDate = filters.startDate;
if (filters.endDate) params.endDate = filters.endDate;
const response = await api.get('/analytics/stats', { params });
return response.data;
},
async getTrends(days = 30) {
const response = await api.get('/analytics/trends', { params: { days } });
return response.data;
},
async getAgentTrends(days = 30) {
const response = await api.get('/analytics/agent-trends', { params: { days } });
return response.data;
}
};
export const usersAPI = {
async getAll() {
const response = await api.get('/users');
return response.data;
},
async getById(id) {
const response = await api.get(`/users/${id}`);
return response.data;
},
async update(id, userData) {
const response = await api.put(`/users/${id}`, userData);
return response.data;
},
async create(userData) {
const response = await api.post('/users', userData);
return response.data;
},
async delete(id) {
const response = await api.delete(`/users/${id}`);
return response.data;
},
async toggleStatus(id) {
const response = await api.patch(`/users/${id}/toggle-status`);
return response.data;
}
};
export default api