Switch to HTTP Basic Auth and improve PDF detection
- Replace token-based auth with HTTP Basic Authentication per Binect API v1 spec - Improve PDF detection: check current tab first, then background service, fallback to recent downloads - Add password visibility toggle in login form - Add extensive debug logging throughout for troubleshooting - Update manifest with alarms, activeTab permissions and <all_urls> host permission - Add documentation files and development helper scripts - Add Binect API specs for reference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
0be7b56506
commit
be4377253e
16 changed files with 5079 additions and 114 deletions
|
|
@ -133,6 +133,46 @@ body {
|
|||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
/* Password Input Wrapper */
|
||||
.password-input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.password-input-wrapper input {
|
||||
padding-right: 44px; /* Make room for the eye icon */
|
||||
}
|
||||
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
background: var(--light-bg);
|
||||
color: var(--binect-blue);
|
||||
}
|
||||
|
||||
.password-toggle:focus {
|
||||
outline: 2px solid var(--binect-blue);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.password-toggle svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>BinectChrome</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
|
@ -26,7 +25,19 @@
|
|||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
<div class="password-input-wrapper">
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
<button type="button" id="togglePassword" class="password-toggle" aria-label="Show password" title="Show password">
|
||||
<svg id="eyeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
<svg id="eyeOffIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
|
||||
<line x1="1" y1="1" x2="23" y2="23"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" id="loginBtn">Sign In</button>
|
||||
|
|
@ -39,7 +50,7 @@
|
|||
<div id="mainView" class="view" style="display: none;">
|
||||
<!-- No PDF Detected -->
|
||||
<div id="noPdfView" class="content-section">
|
||||
<p class="info-text">No PDF detected. Download a PDF to get started.</p>
|
||||
<p class="info-text">No PDF detected. Open or download a PDF to get started.</p>
|
||||
</div>
|
||||
|
||||
<!-- PDF Detected -->
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
* Popup UI Logic
|
||||
*/
|
||||
|
||||
import './popup.css';
|
||||
import { loadCredentials, saveCredentials, deleteCredentials, updateLastUse } from '../utils/storage';
|
||||
import { authenticate, uploadPDF, BinectAPIError } from '../utils/binect-api';
|
||||
import { uploadPDF, testConnection, BinectAPIError } from '../utils/binect-api';
|
||||
import { fetchPDFBytes, DetectedPDF } from '../utils/pdf-detector';
|
||||
import { addTrackingEntry } from '../tracking/tracker';
|
||||
|
||||
|
|
@ -29,10 +30,13 @@ const statusMessage = document.getElementById('statusMessage')!;
|
|||
|
||||
const logoutBtn = document.getElementById('logoutBtn')!;
|
||||
const helpBtn = document.getElementById('helpBtn')!;
|
||||
const togglePasswordBtn = document.getElementById('togglePassword') as HTMLButtonElement;
|
||||
const eyeIcon = document.getElementById('eyeIcon')!;
|
||||
const eyeOffIcon = document.getElementById('eyeOffIcon')!;
|
||||
|
||||
// State
|
||||
let currentPDF: DetectedPDF | null = null;
|
||||
let authToken: string | null = null;
|
||||
let currentCredentials: { username: string; password: string } | null = null;
|
||||
|
||||
/**
|
||||
* Initialize popup
|
||||
|
|
@ -42,16 +46,23 @@ async function init() {
|
|||
const credentials = await loadCredentials();
|
||||
|
||||
if (credentials) {
|
||||
// Try to authenticate
|
||||
// Try to test connection
|
||||
try {
|
||||
const token = await authenticate(credentials.username, credentials.password);
|
||||
authToken = token.token;
|
||||
await updateLastUse();
|
||||
const isConnected = await testConnection(credentials.username, credentials.password);
|
||||
|
||||
showMainView();
|
||||
await loadLastPDF();
|
||||
if (isConnected) {
|
||||
currentCredentials = credentials;
|
||||
await updateLastUse();
|
||||
|
||||
showMainView();
|
||||
await loadLastPDF();
|
||||
} else {
|
||||
// Authentication failed, credentials may be invalid
|
||||
showAuthView();
|
||||
}
|
||||
} catch (error) {
|
||||
// Authentication failed, credentials may be invalid
|
||||
// Connection test failed
|
||||
console.error('[Popup] Connection test failed:', error);
|
||||
showAuthView();
|
||||
}
|
||||
} else {
|
||||
|
|
@ -70,6 +81,30 @@ function setupEventListeners() {
|
|||
sendBtn.addEventListener('click', handleSendPDF);
|
||||
logoutBtn.addEventListener('click', handleLogout);
|
||||
helpBtn.addEventListener('click', handleHelp);
|
||||
togglePasswordBtn.addEventListener('click', handleTogglePassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle password visibility toggle
|
||||
*/
|
||||
function handleTogglePassword() {
|
||||
const isPassword = passwordInput.type === 'password';
|
||||
|
||||
if (isPassword) {
|
||||
// Show password
|
||||
passwordInput.type = 'text';
|
||||
eyeIcon.style.display = 'none';
|
||||
eyeOffIcon.style.display = 'block';
|
||||
togglePasswordBtn.setAttribute('aria-label', 'Hide password');
|
||||
togglePasswordBtn.setAttribute('title', 'Hide password');
|
||||
} else {
|
||||
// Hide password
|
||||
passwordInput.type = 'password';
|
||||
eyeIcon.style.display = 'block';
|
||||
eyeOffIcon.style.display = 'none';
|
||||
togglePasswordBtn.setAttribute('aria-label', 'Show password');
|
||||
togglePasswordBtn.setAttribute('title', 'Show password');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,10 +126,15 @@ async function handleLogin(e: Event) {
|
|||
hideError();
|
||||
|
||||
try {
|
||||
const token = await authenticate(username, password);
|
||||
authToken = token.token;
|
||||
const isConnected = await testConnection(username, password);
|
||||
|
||||
if (!isConnected) {
|
||||
showError('Invalid credentials. Please check your username and password.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save credentials
|
||||
currentCredentials = { username, password };
|
||||
await saveCredentials({ username, password });
|
||||
|
||||
showMainView();
|
||||
|
|
@ -115,7 +155,7 @@ async function handleLogin(e: Event) {
|
|||
* Handle send PDF
|
||||
*/
|
||||
async function handleSendPDF() {
|
||||
if (!currentPDF || !authToken) {
|
||||
if (!currentPDF || !currentCredentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -126,15 +166,20 @@ async function handleSendPDF() {
|
|||
// Fetch PDF bytes
|
||||
const pdfBytes = await fetchPDFBytes(currentPDF.url);
|
||||
|
||||
// Upload to Binect
|
||||
const result = await uploadPDF(pdfBytes, currentPDF.filename, authToken);
|
||||
// Upload to Binect with credentials
|
||||
const document = await uploadPDF(
|
||||
pdfBytes,
|
||||
currentPDF.filename,
|
||||
currentCredentials.username,
|
||||
currentCredentials.password
|
||||
);
|
||||
|
||||
// Track successful transfer
|
||||
await addTrackingEntry({
|
||||
timestamp: Date.now(),
|
||||
sourceDomain: currentPDF.sourceDomain,
|
||||
destinationUrl: 'https://api.binect.de/documents/upload',
|
||||
pdfSize: currentPDF.size,
|
||||
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
|
||||
pdfSize: pdfBytes.byteLength, // Use actual size from fetched data
|
||||
result: 'success'
|
||||
});
|
||||
|
||||
|
|
@ -144,7 +189,7 @@ async function handleSendPDF() {
|
|||
// Notify background script
|
||||
chrome.runtime.sendMessage({ action: 'pdfSent' });
|
||||
|
||||
showStatus(`Success! Document ID: ${result.documentId}`, 'success');
|
||||
showStatus(`Success! Document ID: ${document.id} (Status: ${document.status.text})`, 'success');
|
||||
|
||||
// Clear PDF after 3 seconds
|
||||
setTimeout(() => {
|
||||
|
|
@ -159,8 +204,8 @@ async function handleSendPDF() {
|
|||
errorMessage = error.message;
|
||||
|
||||
// If auth error, might need to re-login
|
||||
if (error.statusCode === 401) {
|
||||
errorMessage = 'Session expired. Please sign in again.';
|
||||
if (error.statusCode === 401 || error.statusCode === 403) {
|
||||
errorMessage = 'Invalid credentials. Please sign in again.';
|
||||
setTimeout(() => {
|
||||
handleLogout();
|
||||
}, 2000);
|
||||
|
|
@ -173,8 +218,8 @@ async function handleSendPDF() {
|
|||
await addTrackingEntry({
|
||||
timestamp: Date.now(),
|
||||
sourceDomain: currentPDF.sourceDomain,
|
||||
destinationUrl: 'https://api.binect.de/documents/upload',
|
||||
pdfSize: currentPDF.size,
|
||||
destinationUrl: 'https://api.binect.de/binectapi/v1/documents',
|
||||
pdfSize: currentPDF.size || 0,
|
||||
result: 'failure',
|
||||
errorMessage
|
||||
});
|
||||
|
|
@ -190,7 +235,7 @@ async function handleSendPDF() {
|
|||
*/
|
||||
async function handleLogout() {
|
||||
await deleteCredentials();
|
||||
authToken = null;
|
||||
currentCredentials = null;
|
||||
currentPDF = null;
|
||||
|
||||
// Clear form
|
||||
|
|
@ -211,21 +256,156 @@ function handleHelp() {
|
|||
* Load last detected PDF
|
||||
*/
|
||||
async function loadLastPDF() {
|
||||
// Ask background script for last PDF
|
||||
chrome.runtime.sendMessage({ action: 'getLastPDF' }, (response) => {
|
||||
if (response && response.pdf) {
|
||||
console.log('[Popup] Loading last PDF...');
|
||||
|
||||
// First, check if current tab is viewing a PDF
|
||||
const currentTabPDF = await checkCurrentTabForPDF();
|
||||
|
||||
if (currentTabPDF) {
|
||||
console.log('[Popup] Found PDF in current tab:', currentTabPDF.filename);
|
||||
currentPDF = currentTabPDF;
|
||||
showPDF(currentPDF);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Popup] No PDF in current tab, checking background script...');
|
||||
|
||||
// If no PDF in current tab, ask background script for last detected download
|
||||
chrome.runtime.sendMessage({ action: 'getLastPDF' }, async (response) => {
|
||||
if (response && response.pdf && response.pdf !== null) {
|
||||
console.log('[Popup] Background returned PDF:', response.pdf.filename);
|
||||
currentPDF = response.pdf;
|
||||
if (currentPDF) {
|
||||
showPDF(currentPDF);
|
||||
showPDF(response.pdf);
|
||||
} else {
|
||||
console.log('[Popup] Background has no PDF, checking recent downloads as fallback...');
|
||||
|
||||
// Fallback: Check recent downloads directly
|
||||
const recentPDF = await checkRecentDownloads();
|
||||
if (recentPDF !== null) {
|
||||
console.log('[Popup] Found recent PDF download:', recentPDF.filename);
|
||||
currentPDF = recentPDF;
|
||||
showPDF(recentPDF);
|
||||
} else {
|
||||
console.log('[Popup] No PDF found anywhere');
|
||||
showNoPDF();
|
||||
}
|
||||
} else {
|
||||
showNoPDF();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check recent downloads for PDFs (fallback mechanism)
|
||||
*/
|
||||
async function checkRecentDownloads(): Promise<DetectedPDF | null> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.downloads.search(
|
||||
{
|
||||
limit: 20, // Check last 20 downloads
|
||||
orderBy: ['-startTime']
|
||||
},
|
||||
(items) => {
|
||||
console.log('[Popup] Checked recent downloads:', items.length, 'items');
|
||||
|
||||
// Find most recent completed PDF
|
||||
const pdfItem = items.find(
|
||||
(item) =>
|
||||
item.state === 'complete' &&
|
||||
(item.filename.toLowerCase().endsWith('.pdf') || item.mime === 'application/pdf')
|
||||
);
|
||||
|
||||
if (pdfItem) {
|
||||
console.log('[Popup] Found recent PDF:', pdfItem.filename);
|
||||
|
||||
// Extract domain
|
||||
let domain = 'unknown';
|
||||
try {
|
||||
const urlObj = new URL(pdfItem.url);
|
||||
domain = urlObj.hostname;
|
||||
} catch (e) {
|
||||
// Keep default
|
||||
}
|
||||
|
||||
resolve({
|
||||
id: `download-${pdfItem.id}`,
|
||||
filename: pdfItem.filename.split('/').pop() || pdfItem.filename,
|
||||
url: pdfItem.url,
|
||||
size: pdfItem.fileSize,
|
||||
timestamp: Date.now(), // Use current time as approximation
|
||||
sourceDomain: domain
|
||||
});
|
||||
} else {
|
||||
console.log('[Popup] No recent PDF downloads found');
|
||||
resolve(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current active tab is viewing a PDF
|
||||
*/
|
||||
async function checkCurrentTabForPDF(): Promise<DetectedPDF | null> {
|
||||
try {
|
||||
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!tab || !tab.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the URL is a PDF
|
||||
const url = tab.url;
|
||||
const isPDF = url.toLowerCase().endsWith('.pdf') ||
|
||||
url.includes('type=application/pdf') ||
|
||||
url.includes('mime=application/pdf');
|
||||
|
||||
if (isPDF) {
|
||||
// Extract filename from URL
|
||||
let filename = 'document.pdf';
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const pathname = urlObj.pathname;
|
||||
const pathParts = pathname.split('/');
|
||||
const lastPart = pathParts[pathParts.length - 1];
|
||||
if (lastPart && lastPart.toLowerCase().endsWith('.pdf')) {
|
||||
filename = decodeURIComponent(lastPart);
|
||||
} else if (tab.title && tab.title !== 'about:blank' && !tab.title.startsWith('chrome://')) {
|
||||
// Use tab title if available
|
||||
filename = tab.title.endsWith('.pdf') ? tab.title : `${tab.title}.pdf`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Use tab title as fallback
|
||||
if (tab.title && tab.title !== 'about:blank') {
|
||||
filename = tab.title.endsWith('.pdf') ? tab.title : `${tab.title}.pdf`;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract domain
|
||||
let domain = 'unknown';
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
domain = urlObj.hostname;
|
||||
} catch (e) {
|
||||
// Keep default
|
||||
}
|
||||
|
||||
return {
|
||||
id: `tab-${tab.id}`,
|
||||
filename,
|
||||
url,
|
||||
size: 0, // Unknown size for viewed PDFs
|
||||
timestamp: Date.now(),
|
||||
sourceDomain: domain
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error checking current tab for PDF:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show auth view
|
||||
*/
|
||||
|
|
@ -301,7 +481,9 @@ function hideStatus() {
|
|||
* Format file size
|
||||
*/
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
if (bytes === 0) {
|
||||
return 'Size unknown';
|
||||
} else if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue