Skip to content

Commit c8d7456

Browse files
committed
fix: made the bad authentication more user-friendly
1 parent 6a65170 commit c8d7456

3 files changed

Lines changed: 233 additions & 5 deletions

File tree

webapp/app.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ def report_collection_progress(collected: int, expected_total: int):
189189
progress.set_results(unique_businesses)
190190

191191
except DecodoUnauthorizedError as e:
192-
progress.update(status="error", progress=100, message=str(e))
193-
progress.set_error(f"AUTH_REQUIRED::{str(e)}")
192+
error_message = "Invalid username or password. Please check your Decodo API credentials and try again."
193+
progress.update(status="error", progress=100, message=error_message)
194+
progress.set_error(f"AUTH_REQUIRED::{error_message}")
194195
except Exception as e:
195196
progress.update(status="error", progress=100, message=str(e))
196197
progress.set_error(str(e))
@@ -214,7 +215,10 @@ def start_search():
214215
password = request.headers.get('X-Decodo-Password')
215216

216217
if not username or not password:
217-
return jsonify({'error': 'Missing API credentials. Please configure your Decodo credentials.'}), 401
218+
return jsonify({
219+
'error': 'Missing API credentials. Please configure your Decodo username and password.',
220+
'auth_required': True
221+
}), 401
218222

219223
try:
220224
data = request.get_json()

webapp/static/css/styles.css

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,121 @@ body {
13981398
transform: scale(1.2);
13991399
}
14001400

1401+
/* Error Alert Styles */
1402+
.error-alert {
1403+
display: flex;
1404+
align-items: flex-start;
1405+
gap: var(--spacing-md);
1406+
padding: var(--spacing-lg);
1407+
background: #FEF2F2;
1408+
border: 2px solid #FCA5A5;
1409+
border-left: 4px solid var(--danger);
1410+
border-radius: var(--radius-lg);
1411+
margin-bottom: var(--spacing-xl);
1412+
box-shadow: var(--shadow-md);
1413+
animation: slideIn 0.3s ease;
1414+
transition: opacity 0.3s ease;
1415+
}
1416+
1417+
.error-alert svg {
1418+
flex-shrink: 0;
1419+
color: var(--danger);
1420+
margin-top: 2px;
1421+
}
1422+
1423+
.error-content {
1424+
flex: 1;
1425+
min-width: 0;
1426+
}
1427+
1428+
.error-content strong {
1429+
display: block;
1430+
font-size: 1rem;
1431+
font-weight: 600;
1432+
color: #991B1B;
1433+
margin-bottom: var(--spacing-xs);
1434+
}
1435+
1436+
.error-content p {
1437+
margin: 0;
1438+
color: #7F1D1D;
1439+
font-size: 0.95rem;
1440+
line-height: 1.5;
1441+
}
1442+
1443+
.error-close {
1444+
flex-shrink: 0;
1445+
background: none;
1446+
border: none;
1447+
font-size: 1.5rem;
1448+
font-weight: 700;
1449+
color: var(--danger);
1450+
cursor: pointer;
1451+
line-height: 1;
1452+
padding: 0;
1453+
width: 28px;
1454+
height: 28px;
1455+
border-radius: var(--radius-sm);
1456+
transition: all 0.2s ease;
1457+
}
1458+
1459+
.error-close:hover {
1460+
background: rgba(239, 68, 68, 0.1);
1461+
transform: scale(1.1);
1462+
}
1463+
1464+
/* Auth Error Alert (inside modal) */
1465+
.auth-error-alert {
1466+
display: flex;
1467+
align-items: flex-start;
1468+
gap: var(--spacing-md);
1469+
padding: var(--spacing-lg);
1470+
background: #FEF2F2;
1471+
border: 2px solid #FCA5A5;
1472+
border-left: 4px solid var(--danger);
1473+
border-radius: var(--radius-lg);
1474+
margin-bottom: var(--spacing-xl);
1475+
animation: slideIn 0.3s ease;
1476+
}
1477+
1478+
.auth-error-alert svg {
1479+
flex-shrink: 0;
1480+
color: var(--danger);
1481+
margin-top: 2px;
1482+
}
1483+
1484+
.auth-error-content {
1485+
flex: 1;
1486+
}
1487+
1488+
.auth-error-content strong {
1489+
display: block;
1490+
font-size: 1rem;
1491+
font-weight: 600;
1492+
color: #991B1B;
1493+
margin-bottom: var(--spacing-xs);
1494+
}
1495+
1496+
.auth-error-content p {
1497+
margin: 0;
1498+
color: #7F1D1D;
1499+
font-size: 0.95rem;
1500+
line-height: 1.5;
1501+
}
1502+
1503+
/* Input Error States */
1504+
.form-group input.error,
1505+
.form-group select.error {
1506+
border-color: var(--danger);
1507+
background: #FEF2F2;
1508+
}
1509+
1510+
.form-group input.error:focus,
1511+
.form-group select.error:focus {
1512+
border-color: var(--danger);
1513+
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
1514+
}
1515+
14011516
/* Responsive Design for Map */
14021517
@media (max-width: 768px) {
14031518
.location-mode-toggle {
@@ -1445,4 +1560,13 @@ body {
14451560
justify-content: center;
14461561
margin-bottom: var(--spacing-md);
14471562
}
1563+
1564+
.error-alert {
1565+
flex-direction: column;
1566+
gap: var(--spacing-sm);
1567+
}
1568+
1569+
.error-close {
1570+
align-self: flex-end;
1571+
}
14481572
}

webapp/static/js/app.js

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -700,11 +700,18 @@ async function handleSearch(e) {
700700
// Check if response is OK
701701
if (!response.ok) {
702702
let errorMessage = 'Failed to start search';
703+
let isAuthError = false;
703704

704705
// Try to parse error as JSON
705706
try {
706707
const error = await response.json();
707708
errorMessage = error.error || errorMessage;
709+
710+
// Check if this is an authentication error
711+
if (response.status === 401 || error.auth_required) {
712+
isAuthError = true;
713+
errorMessage = 'AUTH_REQUIRED::' + errorMessage;
714+
}
708715
} catch (jsonError) {
709716
// If JSON parsing fails, try to get text
710717
try {
@@ -718,6 +725,12 @@ async function handleSearch(e) {
718725
// If all else fails, use status text
719726
errorMessage = `Server error: ${response.status} ${response.statusText}`;
720727
}
728+
729+
// Check for 401 status
730+
if (response.status === 401) {
731+
isAuthError = true;
732+
errorMessage = 'AUTH_REQUIRED::Invalid username or password. Please check your Decodo API credentials.';
733+
}
721734
}
722735

723736
throw new Error(errorMessage);
@@ -1105,11 +1118,98 @@ function showError(message) {
11051118
text = text.slice(authPrefix.length).trim();
11061119
localStorage.removeItem(CREDENTIALS_KEY);
11071120
showCredentialsModal();
1108-
alert(`Authentication Error: ${text}`);
1121+
showAuthErrorInModal(text);
11091122
return;
11101123
}
11111124

1112-
alert(`Error: ${text}`);
1125+
showErrorAlert(text);
1126+
}
1127+
1128+
// Show authentication error in modal
1129+
function showAuthErrorInModal(message) {
1130+
const modalBody = document.querySelector('#credentialsForm');
1131+
1132+
// Remove any existing error alert
1133+
const existingAlert = modalBody.querySelector('.auth-error-alert');
1134+
if (existingAlert) {
1135+
existingAlert.remove();
1136+
}
1137+
1138+
// Create error alert
1139+
const errorAlert = document.createElement('div');
1140+
errorAlert.className = 'auth-error-alert';
1141+
errorAlert.innerHTML = `
1142+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1143+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" fill="currentColor"/>
1144+
</svg>
1145+
<div class="auth-error-content">
1146+
<strong>Authentication Failed</strong>
1147+
<p>${escapeHtml(message)}</p>
1148+
</div>
1149+
`;
1150+
1151+
// Insert at the top of the form
1152+
modalBody.insertBefore(errorAlert, modalBody.firstChild);
1153+
1154+
// Add error state to input fields
1155+
const usernameInput = document.getElementById('decodUsername');
1156+
const passwordInput = document.getElementById('decodPassword');
1157+
if (usernameInput) usernameInput.classList.add('error');
1158+
if (passwordInput) passwordInput.classList.add('error');
1159+
1160+
// Remove error state when user starts typing
1161+
if (usernameInput) {
1162+
usernameInput.addEventListener('input', function removeError() {
1163+
usernameInput.classList.remove('error');
1164+
usernameInput.removeEventListener('input', removeError);
1165+
});
1166+
}
1167+
if (passwordInput) {
1168+
passwordInput.addEventListener('input', function removeError() {
1169+
passwordInput.classList.remove('error');
1170+
passwordInput.removeEventListener('input', removeError);
1171+
});
1172+
}
1173+
1174+
// Scroll error into view
1175+
errorAlert.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1176+
}
1177+
1178+
// Show error alert
1179+
function showErrorAlert(message) {
1180+
// Create a styled alert element
1181+
const alertDiv = document.createElement('div');
1182+
alertDiv.className = 'error-alert';
1183+
alertDiv.innerHTML = `
1184+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1185+
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" fill="currentColor"/>
1186+
</svg>
1187+
<div class="error-content">
1188+
<strong>Error</strong>
1189+
<p>${escapeHtml(message)}</p>
1190+
</div>
1191+
<button class="error-close" onclick="this.parentElement.remove()">&times;</button>
1192+
`;
1193+
1194+
// Add to the page
1195+
const container = document.querySelector('.container');
1196+
const mainContent = document.querySelector('.main-content');
1197+
if (mainContent) {
1198+
mainContent.insertBefore(alertDiv, mainContent.firstChild);
1199+
} else {
1200+
container.insertBefore(alertDiv, container.firstChild);
1201+
}
1202+
1203+
// Scroll into view
1204+
alertDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1205+
1206+
// Auto-remove after 10 seconds
1207+
setTimeout(() => {
1208+
if (alertDiv.parentElement) {
1209+
alertDiv.style.opacity = '0';
1210+
setTimeout(() => alertDiv.remove(), 300);
1211+
}
1212+
}, 10000);
11131213
}
11141214

11151215
// Format Status

0 commit comments

Comments
 (0)