✅ 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>
259 lines
No EOL
6.9 KiB
JavaScript
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 |