- Update configuration for production HTTPS domain (devden.hiddenden.cafe) - Add nginx reverse proxy for /api and /auth routes to backend - Create auth-callback.html to handle Microsoft Entra ID OAuth redirect - Fix API_URL in script.js to use same origin (remove :8000 port) - Add cache-busting query parameter (?v=2) to script.js - Update .env.example with HTTPS requirements documentation This resolves Azure Entra ID redirect URI mismatch and enables proper OAuth authentication flow through the nginx frontend proxy. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
637 lines
17 KiB
JavaScript
637 lines
17 KiB
JavaScript
// Enhanced logging utility
|
|
class Logger {
|
|
constructor() {
|
|
this.logs = [];
|
|
this.maxLogs = 1000;
|
|
}
|
|
|
|
log(level, message, data = null) {
|
|
const timestamp = new Date().toISOString();
|
|
const logEntry = {
|
|
timestamp,
|
|
level,
|
|
message,
|
|
data,
|
|
url: window.location.href
|
|
};
|
|
|
|
this.logs.push(logEntry);
|
|
if (this.logs.length > this.maxLogs) {
|
|
this.logs.shift();
|
|
}
|
|
|
|
const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log';
|
|
console[consoleMethod](`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || '');
|
|
|
|
// Save to localStorage for persistence
|
|
try {
|
|
localStorage.setItem('devden_logs', JSON.stringify(this.logs));
|
|
} catch (e) {
|
|
console.warn('Failed to save logs to localStorage:', e);
|
|
}
|
|
}
|
|
|
|
info(message, data = null) { this.log('info', message, data); }
|
|
warn(message, data = null) { this.log('warn', message, data); }
|
|
error(message, data = null) { this.log('error', message, data); }
|
|
|
|
exportLogs() {
|
|
const blob = new Blob([JSON.stringify(this.logs, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `devden_logs_${new Date().toISOString().split('T')[0]}.json`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
clearLogs() {
|
|
this.logs = [];
|
|
localStorage.removeItem('devden_logs');
|
|
}
|
|
}
|
|
|
|
const logger = new Logger();
|
|
|
|
// Load existing logs from localStorage
|
|
try {
|
|
const savedLogs = localStorage.getItem('devden_logs');
|
|
if (savedLogs) {
|
|
logger.logs = JSON.parse(savedLogs);
|
|
logger.info('Loaded existing logs from localStorage', { count: logger.logs.length });
|
|
}
|
|
} catch (e) {
|
|
logger.warn('Failed to load existing logs from localStorage:', e);
|
|
}
|
|
|
|
const loginScreen = document.getElementById("loginScreen");
|
|
const welcomeScreen = document.getElementById("welcomeScreen");
|
|
const chatScreen = document.getElementById("chatScreen");
|
|
const chatMessages = document.getElementById("chatMessages");
|
|
const welcomeInput = document.getElementById("welcomeInput");
|
|
const chatInput = document.getElementById("chatInput");
|
|
const loginBtn = document.getElementById("loginBtn");
|
|
|
|
// API URL is same as frontend (nginx proxies /api and /auth to backend)
|
|
const API_URL = window.location.origin;
|
|
|
|
let isInChat = false;
|
|
|
|
// Auth functions
|
|
function getToken() {
|
|
try {
|
|
const token = localStorage.getItem("devden_token");
|
|
logger.info('Retrieved token from localStorage', { hasToken: !!token });
|
|
return token;
|
|
} catch (error) {
|
|
logger.error('Failed to retrieve token from localStorage', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function setToken(token) {
|
|
try {
|
|
localStorage.setItem("devden_token", token);
|
|
logger.info('Token saved to localStorage');
|
|
} catch (error) {
|
|
logger.error('Failed to save token to localStorage', error);
|
|
}
|
|
}
|
|
|
|
function clearToken() {
|
|
try {
|
|
localStorage.removeItem("devden_token");
|
|
logger.info('Token cleared from localStorage');
|
|
} catch (error) {
|
|
logger.error('Failed to clear token from localStorage', error);
|
|
}
|
|
}
|
|
|
|
function showLoginScreen() {
|
|
try {
|
|
loginScreen.classList.remove("hidden");
|
|
welcomeScreen.classList.add("hidden");
|
|
chatScreen.classList.add("hidden");
|
|
logger.info('Switched to login screen');
|
|
} catch (error) {
|
|
logger.error('Failed to show login screen', error);
|
|
}
|
|
}
|
|
|
|
function showWelcomeScreen() {
|
|
try {
|
|
loginScreen.classList.add("hidden");
|
|
welcomeScreen.classList.remove("hidden");
|
|
chatScreen.classList.add("hidden");
|
|
welcomeInput.focus();
|
|
logger.info('Switched to welcome screen');
|
|
} catch (error) {
|
|
logger.error('Failed to show welcome screen', error);
|
|
}
|
|
}
|
|
|
|
function switchToChat() {
|
|
try {
|
|
loginScreen.classList.add("hidden");
|
|
welcomeScreen.classList.add("hidden");
|
|
chatScreen.classList.remove("hidden");
|
|
chatInput.focus();
|
|
isInChat = true;
|
|
logger.info('Switched to chat screen');
|
|
} catch (error) {
|
|
logger.error('Failed to switch to chat screen', error);
|
|
}
|
|
}
|
|
|
|
async function checkAuth() {
|
|
logger.info('Starting auth check');
|
|
const token = getToken();
|
|
if (!token) {
|
|
logger.info('No token found, showing login screen');
|
|
showLoginScreen();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.info('Making auth check request to /api/auth/me');
|
|
const response = await fetch(`${API_URL}/api/auth/me`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
|
|
logger.info('Auth check response received', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
ok: response.ok
|
|
});
|
|
|
|
if (response.ok) {
|
|
const userData = await response.json();
|
|
logger.info('Auth check successful', { user: userData });
|
|
showWelcomeScreen();
|
|
} else {
|
|
const errorText = await response.text();
|
|
logger.warn('Auth check failed', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
response: errorText
|
|
});
|
|
clearToken();
|
|
showLoginScreen();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Auth check request failed', {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
showLoginScreen();
|
|
}
|
|
}
|
|
|
|
async function handleLogin() {
|
|
logger.info('Login button clicked, starting login process');
|
|
|
|
try {
|
|
loginBtn.disabled = true;
|
|
loginBtn.textContent = "Checking auth config...";
|
|
logger.info('Disabled login button and updated text');
|
|
|
|
// Check if auth is configured
|
|
logger.info('Checking auth configuration via /api/auth/status');
|
|
const statusResponse = await fetch(`${API_URL}/api/auth/status`);
|
|
const statusData = await statusResponse.json();
|
|
|
|
logger.info('Auth status response', {
|
|
status: statusResponse.status,
|
|
configured: statusData.configured
|
|
});
|
|
|
|
if (!statusData.configured) {
|
|
const errorMsg = "Authentication not configured. Please set ENTRA_TENANT_ID, ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET in your .env file.";
|
|
logger.error('Auth not configured', { response: statusData });
|
|
alert(errorMsg);
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = "Sign in with Microsoft";
|
|
return;
|
|
}
|
|
|
|
loginBtn.textContent = "Getting auth URL...";
|
|
logger.info('Auth configured, requesting login URL from /api/auth/login');
|
|
|
|
// Get auth URL and redirect
|
|
const response = await fetch(`${API_URL}/api/auth/login`);
|
|
const data = await response.json();
|
|
|
|
logger.info('Login URL response', {
|
|
status: response.status,
|
|
hasAuthUrl: !!data.auth_url,
|
|
authUrl: data.auth_url ? data.auth_url.substring(0, 100) + '...' : null
|
|
});
|
|
|
|
if (data.auth_url) {
|
|
loginBtn.textContent = "Redirecting...";
|
|
logger.info('Redirecting to Microsoft OAuth URL');
|
|
window.location.href = data.auth_url;
|
|
} else {
|
|
throw new Error("No auth URL returned");
|
|
}
|
|
} catch (error) {
|
|
logger.error('Login process failed', {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
alert("Login failed: " + error.message);
|
|
loginBtn.disabled = false;
|
|
loginBtn.textContent = "Sign in with Microsoft";
|
|
}
|
|
}
|
|
|
|
async function handleCallback() {
|
|
logger.info('Starting OAuth callback processing', { url: window.location.href });
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const code = params.get("code");
|
|
const sessionState = params.get("session_state");
|
|
const error = params.get("error");
|
|
const errorDescription = params.get("error_description");
|
|
|
|
logger.info('Parsed URL parameters', {
|
|
hasCode: !!code,
|
|
hasSessionState: !!sessionState,
|
|
hasError: !!error,
|
|
codeLength: code ? code.length : 0,
|
|
sessionState: sessionState,
|
|
error: error,
|
|
errorDescription: errorDescription
|
|
});
|
|
|
|
// Check for OAuth errors
|
|
if (error) {
|
|
logger.error('OAuth error in callback URL', {
|
|
error: error,
|
|
errorDescription: errorDescription
|
|
});
|
|
alert(`Authentication failed: ${error} - ${errorDescription || 'Unknown error'}`);
|
|
return false;
|
|
}
|
|
|
|
if (!code) {
|
|
logger.warn('No authorization code found in callback URL');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
logger.info('Sending authorization code to backend /api/auth/callback');
|
|
const response = await fetch(`${API_URL}/api/auth/callback`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ code }),
|
|
});
|
|
|
|
logger.info('Callback response received', {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
ok: response.ok
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
logger.error('Callback request failed', {
|
|
status: response.status,
|
|
error: errorData
|
|
});
|
|
throw new Error(errorData.detail || "Callback failed");
|
|
}
|
|
|
|
const data = await response.json();
|
|
logger.info('Callback successful', {
|
|
hasToken: !!data.token,
|
|
hasUser: !!data.user,
|
|
user: data.user
|
|
});
|
|
|
|
setToken(data.token);
|
|
|
|
// Clean up URL
|
|
logger.info('Cleaning up URL (removing query parameters)');
|
|
window.history.replaceState({}, "", "/");
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error('Callback processing failed', {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
alert("Authentication failed: " + error.message);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Chat functions
|
|
function addMessage(text, type = "user") {
|
|
const msg = document.createElement("div");
|
|
msg.className = `message ${type}`;
|
|
msg.innerHTML = `<div class="message-text">${escapeHtml(text)}</div>`;
|
|
chatMessages.appendChild(msg);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function showTyping() {
|
|
const typing = document.createElement("div");
|
|
typing.className = "message assistant";
|
|
typing.id = "typing";
|
|
typing.innerHTML = `
|
|
<div class="message-text">
|
|
<div class="typing">
|
|
<span></span>
|
|
<span></span>
|
|
<span></span>
|
|
</div>
|
|
</div>
|
|
`;
|
|
chatMessages.appendChild(typing);
|
|
scrollToBottom();
|
|
}
|
|
|
|
function hideTyping() {
|
|
const typing = document.getElementById("typing");
|
|
if (typing) typing.remove();
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
async function sendMessage(text) {
|
|
if (!text.trim()) return;
|
|
|
|
if (!isInChat) {
|
|
switchToChat();
|
|
}
|
|
|
|
addMessage(text, "user");
|
|
chatInput.value = "";
|
|
chatInput.disabled = true;
|
|
|
|
showTyping();
|
|
|
|
try {
|
|
const token = getToken();
|
|
const response = await fetch(`${API_URL}/api/chat/stream`, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify({
|
|
message: text,
|
|
provider: null,
|
|
}),
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
hideTyping();
|
|
clearToken();
|
|
showLoginScreen();
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
hideTyping();
|
|
|
|
// Create message element for streaming
|
|
const msgDiv = document.createElement("div");
|
|
msgDiv.className = "message assistant";
|
|
const textDiv = document.createElement("div");
|
|
textDiv.className = "message-text";
|
|
msgDiv.appendChild(textDiv);
|
|
chatMessages.appendChild(msgDiv);
|
|
|
|
// Read stream
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let assistantMessage = "";
|
|
|
|
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: ")) {
|
|
const data = JSON.parse(line.slice(6));
|
|
|
|
if (data.error) {
|
|
textDiv.textContent = `Error: ${data.error}`;
|
|
break;
|
|
}
|
|
|
|
if (data.chunk) {
|
|
assistantMessage += data.chunk;
|
|
textDiv.textContent = assistantMessage;
|
|
scrollToBottom();
|
|
}
|
|
|
|
if (data.done) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
hideTyping();
|
|
addMessage(`Error: ${error.message}`, "assistant");
|
|
} finally {
|
|
chatInput.disabled = false;
|
|
chatInput.focus();
|
|
}
|
|
}
|
|
|
|
// Event listeners
|
|
loginBtn.addEventListener("click", handleLogin);
|
|
|
|
welcomeInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
const text = welcomeInput.value.trim();
|
|
if (text) {
|
|
sendMessage(text);
|
|
}
|
|
}
|
|
});
|
|
|
|
chatInput.addEventListener("keydown", (e) => {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
sendMessage(chatInput.value);
|
|
}
|
|
});
|
|
|
|
// Initialize
|
|
async function init() {
|
|
logger.info('Application initialization started', {
|
|
userAgent: navigator.userAgent,
|
|
url: window.location.href,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Check for CSS loading
|
|
const styleLink = document.querySelector('link[rel="stylesheet"]');
|
|
if (styleLink) {
|
|
logger.info('CSS link found', { href: styleLink.href });
|
|
styleLink.addEventListener('load', () => {
|
|
logger.info('CSS loaded successfully');
|
|
});
|
|
styleLink.addEventListener('error', (e) => {
|
|
logger.error('CSS failed to load', { href: styleLink.href, error: e });
|
|
});
|
|
} else {
|
|
logger.error('CSS link not found in document');
|
|
}
|
|
|
|
// Check DOM elements
|
|
const elements = ['loginScreen', 'welcomeScreen', 'chatScreen', 'chatMessages', 'welcomeInput', 'chatInput', 'loginBtn'];
|
|
elements.forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (!el) {
|
|
logger.error(`Required DOM element not found: ${id}`);
|
|
} else {
|
|
logger.info(`DOM element found: ${id}`);
|
|
}
|
|
});
|
|
|
|
try {
|
|
// Check for OAuth callback first
|
|
logger.info('Checking for OAuth callback parameters');
|
|
const callbackSuccess = await handleCallback();
|
|
|
|
if (callbackSuccess) {
|
|
logger.info('OAuth callback processed successfully, showing welcome screen');
|
|
showWelcomeScreen();
|
|
} else {
|
|
logger.info('No OAuth callback or callback failed, checking existing auth');
|
|
await checkAuth();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Initialization failed', {
|
|
error: error.message,
|
|
stack: error.stack
|
|
});
|
|
}
|
|
|
|
logger.info('Application initialization completed');
|
|
}
|
|
|
|
// Add global error handler
|
|
window.addEventListener('error', (event) => {
|
|
logger.error('Global JavaScript error', {
|
|
message: event.message,
|
|
filename: event.filename,
|
|
lineno: event.lineno,
|
|
colno: event.colno,
|
|
error: event.error
|
|
});
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', (event) => {
|
|
logger.error('Unhandled promise rejection', {
|
|
reason: event.reason,
|
|
promise: event.promise
|
|
});
|
|
});
|
|
|
|
// Debug panel functionality
|
|
const debugPanel = document.getElementById('debugPanel');
|
|
const exportLogsBtn = document.getElementById('exportLogsBtn');
|
|
const clearLogsBtn = document.getElementById('clearLogsBtn');
|
|
const viewBackendLogsBtn = document.getElementById('viewBackendLogsBtn');
|
|
const closeDebugBtn = document.getElementById('closeDebugBtn');
|
|
const debugOutput = document.getElementById('debugOutput');
|
|
|
|
function showDebugPanel() {
|
|
debugPanel.style.display = 'block';
|
|
logger.info('Debug panel opened');
|
|
}
|
|
|
|
function hideDebugPanel() {
|
|
debugPanel.style.display = 'none';
|
|
logger.info('Debug panel closed');
|
|
}
|
|
|
|
function updateDebugOutput(text) {
|
|
debugOutput.textContent = text;
|
|
debugOutput.scrollTop = debugOutput.scrollHeight;
|
|
}
|
|
|
|
// Event listeners for debug panel
|
|
exportLogsBtn.addEventListener('click', () => {
|
|
logger.exportLogs();
|
|
updateDebugOutput('Frontend logs exported to download');
|
|
logger.info('Logs exported via debug panel');
|
|
});
|
|
|
|
clearLogsBtn.addEventListener('click', () => {
|
|
logger.clearLogs();
|
|
updateDebugOutput('Logs cleared');
|
|
logger.info('Logs cleared via debug panel');
|
|
});
|
|
|
|
viewBackendLogsBtn.addEventListener('click', async () => {
|
|
try {
|
|
updateDebugOutput('Loading backend logs...');
|
|
const response = await fetch(`${API_URL}/logs`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.logs) {
|
|
updateDebugOutput(data.logs.join('\n'));
|
|
} else if (data.error) {
|
|
updateDebugOutput(`Error: ${data.error}`);
|
|
}
|
|
} else {
|
|
updateDebugOutput(`Failed to load backend logs: ${response.status}`);
|
|
}
|
|
} catch (error) {
|
|
updateDebugOutput(`Error loading backend logs: ${error.message}`);
|
|
logger.error('Failed to load backend logs', error);
|
|
}
|
|
});
|
|
|
|
closeDebugBtn.addEventListener('click', hideDebugPanel);
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
// Ctrl+Shift+L: Export logs
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'L') {
|
|
e.preventDefault();
|
|
logger.exportLogs();
|
|
logger.info('Logs exported via keyboard shortcut');
|
|
}
|
|
|
|
// Ctrl+Shift+D: Toggle debug panel
|
|
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
|
|
e.preventDefault();
|
|
if (debugPanel.style.display === 'none') {
|
|
showDebugPanel();
|
|
} else {
|
|
hideDebugPanel();
|
|
}
|
|
}
|
|
|
|
// Escape: Close debug panel
|
|
if (e.key === 'Escape' && debugPanel.style.display !== 'none') {
|
|
hideDebugPanel();
|
|
}
|
|
});
|
|
|
|
init();
|