Update subproject commit to indicate a dirty state
This commit is contained in:
11
.cursor/debug.log
Normal file
11
.cursor/debug.log
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{"location":"BackgroundRippleEffect.jsx:84","message":"Grid cell visibility check","data":{"cellsCount":216,"cellWidth":56,"cellHeight":56,"cellOpacity":"1","cellBackgroundColor":"rgba(255, 255, 255, 0.05)","cellBorderColor":"rgba(255, 255, 255, 0.4)","cellDisplay":"block","rows":8,"cols":27,"cellSize":56},"timestamp":1768696693322,"sessionId":"debug-session","runId":"run1","hypothesisId":"B"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:25","message":"Container visibility check","data":{"containerWidth":1070,"containerHeight":853,"zIndex":"0","opacity":"1","display":"block","visibility":"visible","calculatedCols":27,"calculatedRows":8,"viewportSize":{"width":0,"height":0},"pageBackground":"rgba(0, 0, 0, 0)"},"timestamp":1768696693324,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:84","message":"Grid cell visibility check","data":{"cellsCount":216,"cellWidth":56,"cellHeight":56,"cellOpacity":"1","cellBackgroundColor":"rgba(255, 255, 255, 0.05)","cellBorderColor":"rgba(255, 255, 255, 0.4)","cellDisplay":"block","rows":8,"cols":27,"cellSize":56},"timestamp":1768696693330,"sessionId":"debug-session","runId":"run1","hypothesisId":"B"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:25","message":"Container visibility check","data":{"containerWidth":1070,"containerHeight":853,"zIndex":"0","opacity":"1","display":"block","visibility":"visible","calculatedCols":27,"calculatedRows":8,"viewportSize":{"width":0,"height":0},"pageBackground":"rgba(0, 0, 0, 0)"},"timestamp":1768696693330,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:84","message":"Grid cell visibility check","data":{"cellsCount":396,"cellWidth":56,"cellHeight":56,"cellOpacity":"1","cellBackgroundColor":"rgba(255, 255, 255, 0.05)","cellBorderColor":"rgba(255, 255, 255, 0.4)","cellDisplay":"block","rows":18,"cols":22,"cellSize":56},"timestamp":1768696693374,"sessionId":"debug-session","runId":"run1","hypothesisId":"B"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:25","message":"Container visibility check","data":{"containerWidth":1070,"containerHeight":853,"zIndex":"0","opacity":"1","display":"block","visibility":"visible","calculatedCols":22,"calculatedRows":18,"viewportSize":{"width":1070,"height":853},"pageBackground":"rgba(0, 0, 0, 0)"},"timestamp":1768696693376,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
|
{"location":"ebayParserService.js:341","message":"parseEbayAccount entry","data":{"url":"https://www.ebay.de/str/apecollection?_trksid=p4429486.m3561.l161210"},"timestamp":1768696699523,"sessionId":"debug-session","runId":"run2","hypothesisId":"C"}
|
||||||
|
{"location":"ebayParserService.js:346","message":"parseEbayAccount: isExtensionAvailable result","data":{"extAvailable":false,"willTryExtension":false},"timestamp":1768696699525,"sessionId":"debug-session","runId":"run2","hypothesisId":"C"}
|
||||||
|
{"location":"ebayParserService.js:94","message":"isExtensionAvailable check","data":{"hasFlag":false,"result":false},"timestamp":1768696699524,"sessionId":"debug-session","runId":"run2","hypothesisId":"C"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:84","message":"Grid cell visibility check","data":{"cellsCount":408,"cellWidth":56,"cellHeight":56,"cellOpacity":"1","cellBackgroundColor":"rgba(255, 255, 255, 0.05)","cellBorderColor":"rgba(255, 255, 255, 0.4)","cellDisplay":"block","rows":17,"cols":24,"cellSize":56},"timestamp":1768696852148,"sessionId":"debug-session","runId":"run1","hypothesisId":"B"}
|
||||||
|
{"location":"BackgroundRippleEffect.jsx:25","message":"Container visibility check","data":{"containerWidth":1203,"containerHeight":840,"zIndex":"0","opacity":"1","display":"block","visibility":"visible","calculatedCols":24,"calculatedRows":17,"viewportSize":{"width":1203,"height":840},"pageBackground":"rgba(0, 0, 0, 0)"},"timestamp":1768696852149,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
||||||
@@ -1,189 +1,9 @@
|
|||||||
# EShip Browser Extension
|
# Extension
|
||||||
|
|
||||||
Chrome Extension (Manifest V3) fuer die Authentifizierung und Tool-Verwaltung der EShip Web-App.
|
Dieser Ordner enthält die Browser-Extension, die für Frontend-Funktionalität und Datenextraktion (z.B. von Amazon) zuständig ist.
|
||||||
|
|
||||||
## Funktionen
|
## Geplante Funktionen
|
||||||
|
|
||||||
- **Authentifizierung**: Login via Appwrite (Email/Password)
|
- Frontend-Benutzeroberfläche
|
||||||
- **Website-Schutz**: Blockiert die Web-App fuer nicht-authentifizierte Benutzer
|
- Datenextraktion von Websites (z.B. Amazon)
|
||||||
- **Tools-System**: Aktivierbare Tools mit individuellen Einstellungen
|
- Kommunikation mit dem Server für Verschlüsselung und Authentifizierung
|
||||||
- **Live-Updates**: Einstellungen werden sofort auf der Website angewendet
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
Extension/
|
|
||||||
├── manifest.json # Chrome Extension Manifest (MV3)
|
|
||||||
├── config.js # Appwrite Konfiguration
|
|
||||||
├── background/
|
|
||||||
│ └── service-worker.js # Background Service Worker
|
|
||||||
├── popup/
|
|
||||||
│ ├── popup.html # Popup UI
|
|
||||||
│ ├── popup.css # Popup Styles
|
|
||||||
│ └── popup.js # Popup Logic
|
|
||||||
├── content/
|
|
||||||
│ └── content-script.js # Content Script fuer Website
|
|
||||||
└── lib/
|
|
||||||
└── appwrite.min.js # Appwrite SDK Bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### 1. Appwrite API Key erstellen
|
|
||||||
|
|
||||||
**WICHTIG**: Du musst einen API Key in Appwrite erstellen:
|
|
||||||
|
|
||||||
1. Gehe zu https://cloud.appwrite.io
|
|
||||||
2. Waehle dein Projekt
|
|
||||||
3. Settings > API Keys > "Create API Key"
|
|
||||||
4. Scopes: `users.read`, `users.write`, `sessions.write`
|
|
||||||
5. Kopiere den API Key (wird nur einmal angezeigt!)
|
|
||||||
|
|
||||||
Siehe auch: `setup/API_KEY_SETUP.md` fuer detaillierte Anleitung.
|
|
||||||
|
|
||||||
### 2. Extension konfigurieren
|
|
||||||
|
|
||||||
Bearbeite `Extension/config.js`:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var APPWRITE_CONFIG = {
|
|
||||||
endpoint: 'https://cloud.appwrite.io/v1',
|
|
||||||
projectId: '696b82bb0036d2e547ad',
|
|
||||||
apiKey: 'DEIN_API_KEY_HIER' // WICHTIG: API Key hier einfuegen
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Extension in Chrome laden
|
|
||||||
|
|
||||||
1. Oeffne Chrome und navigiere zu `chrome://extensions`
|
|
||||||
2. Aktiviere den **Entwicklermodus** (Toggle oben rechts)
|
|
||||||
3. Klicke auf **Entpackte Erweiterung laden**
|
|
||||||
4. Waehle den `Extension/` Ordner aus
|
|
||||||
5. Die Extension erscheint in der Toolbar
|
|
||||||
|
|
||||||
### 4. Server starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd Server
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Server startet unter `http://localhost:5173`
|
|
||||||
|
|
||||||
## Testen
|
|
||||||
|
|
||||||
### Auth-Flow testen
|
|
||||||
|
|
||||||
1. **Ohne Login**:
|
|
||||||
- Oeffne `http://localhost:5173`
|
|
||||||
- Die Seite zeigt einen "Zugriff gesperrt" Bildschirm
|
|
||||||
|
|
||||||
2. **Login durchfuehren**:
|
|
||||||
- Klicke auf das Extension-Icon in der Toolbar
|
|
||||||
- Gib deine Appwrite-Zugangsdaten ein
|
|
||||||
- Klicke auf "Anmelden"
|
|
||||||
- Nach erfolgreichem Login wird die Website automatisch geoeffnet
|
|
||||||
|
|
||||||
3. **Nach Login**:
|
|
||||||
- Die Website ist voll zugaenglich
|
|
||||||
- Das Extension-Popup zeigt das Tools-Menu
|
|
||||||
|
|
||||||
### Tools testen
|
|
||||||
|
|
||||||
1. Oeffne das Extension-Popup (nach Login)
|
|
||||||
2. Aktiviere das Tool "Preise hervorheben"
|
|
||||||
3. Auf der Website werden alle `.price` Elemente mit rotem Rahmen hervorgehoben
|
|
||||||
4. Aendere den Selector oder die Farbe in den Tool-Einstellungen
|
|
||||||
5. Die Aenderungen werden sofort angewendet
|
|
||||||
|
|
||||||
### Demo-Preise
|
|
||||||
|
|
||||||
Die Web-App enthaelt Demo-Preiselemente in der unteren rechten Ecke zum Testen des Highlight-Tools.
|
|
||||||
|
|
||||||
## Tools-Registry
|
|
||||||
|
|
||||||
Tools werden in `background/service-worker.js` definiert:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const DEFAULT_TOOLS = [
|
|
||||||
{
|
|
||||||
id: 'highlight_prices',
|
|
||||||
name: 'Preise hervorheben',
|
|
||||||
enabled: false,
|
|
||||||
settings: {
|
|
||||||
selector: '.price',
|
|
||||||
borderColor: '#ff0000',
|
|
||||||
borderWidth: '2px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
### Neues Tool hinzufuegen
|
|
||||||
|
|
||||||
1. Fuege das Tool zur `DEFAULT_TOOLS` Liste hinzu
|
|
||||||
2. Implementiere die Logik in `content/content-script.js`:
|
|
||||||
```javascript
|
|
||||||
function applyTool(tool) {
|
|
||||||
switch (tool.id) {
|
|
||||||
case 'dein_tool_id':
|
|
||||||
applyDeinTool(tool.settings);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Message-Kommunikation
|
|
||||||
|
|
||||||
### Popup <-> Service Worker
|
|
||||||
|
|
||||||
| Action | Beschreibung |
|
|
||||||
|--------|--------------|
|
|
||||||
| `CHECK_AUTH` | Prueft Auth-Status |
|
|
||||||
| `LOGIN` | Login mit Email/Password |
|
|
||||||
| `LOGOUT` | Beendet Session |
|
|
||||||
| `GET_SETTINGS` | Laedt Tool-Einstellungen |
|
|
||||||
| `SAVE_SETTINGS` | Speichert Tool-Einstellungen |
|
|
||||||
|
|
||||||
### Service Worker <-> Content Script
|
|
||||||
|
|
||||||
| Action | Beschreibung |
|
|
||||||
|--------|--------------|
|
|
||||||
| `SETTINGS_UPDATED` | Neue Tool-Einstellungen |
|
|
||||||
| `AUTH_CHANGED` | Auth-Status geaendert |
|
|
||||||
|
|
||||||
## Sicherheit
|
|
||||||
|
|
||||||
- Passwoerter werden **nie** im Klartext gespeichert
|
|
||||||
- Authentifizierung laeuft vollstaendig ueber Appwrite Sessions
|
|
||||||
- Session-Cookies sind HTTP-only und werden von Appwrite verwaltet
|
|
||||||
- Tool-Einstellungen werden in `chrome.storage.sync` gespeichert
|
|
||||||
|
|
||||||
## Fehlerbehebung
|
|
||||||
|
|
||||||
### Extension funktioniert nicht
|
|
||||||
|
|
||||||
1. Pruefe die Browser-Konsole auf Fehler
|
|
||||||
2. Oeffne `chrome://extensions` und klicke auf "Service Worker" bei der Extension
|
|
||||||
3. Pruefe die Konsole des Service Workers
|
|
||||||
|
|
||||||
### Login schlaegt fehl
|
|
||||||
|
|
||||||
1. Pruefe die Appwrite-Konfiguration in `config.js`
|
|
||||||
2. Stelle sicher, dass der **API Key** gesetzt ist
|
|
||||||
3. Pruefe, ob der API Key die richtigen Scopes hat (`users.read`, `users.write`, `sessions.write`)
|
|
||||||
4. Stelle sicher, dass der Appwrite-Benutzer existiert
|
|
||||||
5. Pruefe, ob die Appwrite-Plattform-Einstellungen den Extension-Zugriff erlauben
|
|
||||||
|
|
||||||
### Website bleibt blockiert
|
|
||||||
|
|
||||||
1. Lade die Website neu nach dem Login
|
|
||||||
2. Pruefe, ob die URL in `manifest.json` unter `content_scripts.matches` steht
|
|
||||||
3. Pruefe die Content-Script-Konsole (F12 auf der Website)
|
|
||||||
|
|
||||||
## Bekannte Einschraenkungen
|
|
||||||
|
|
||||||
- Die Extension funktioniert nur auf den konfigurierten URLs (localhost:5173, localhost:3000)
|
|
||||||
- Fuer Produktions-URLs muss `manifest.json` angepasst werden
|
|
||||||
- Icons sind nicht enthalten (Chrome verwendet Standard-Icon)
|
|
||||||
|
|||||||
191
Extension/background.js
Normal file
191
Extension/background.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const STORAGE_KEY = "auth_jwt";
|
||||||
|
const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren
|
||||||
|
|
||||||
|
const PARSE_TIMEOUT_MS = 15000; // 15 seconds
|
||||||
|
const activeParseRequests = new Map(); // Map<tabId, { timeout, originalSender, resolve }>
|
||||||
|
|
||||||
|
// Messages from content script (der von der Web-App kommt)
|
||||||
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
|
// Auth messages vom Content Script
|
||||||
|
if (msg?.type === "AUTH_JWT" && msg.jwt) {
|
||||||
|
chrome.storage.local.set({ [STORAGE_KEY]: msg.jwt }).then(() => {
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
});
|
||||||
|
return true; // async
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg?.type === "AUTH_CLEARED") {
|
||||||
|
chrome.storage.local.remove(STORAGE_KEY).then(() => {
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API calls vom Popup
|
||||||
|
if (msg?.type === "GET_JWT") {
|
||||||
|
getJwt().then(jwt => {
|
||||||
|
sendResponse({ jwt });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg?.type === "CALL_API" && msg.path) {
|
||||||
|
callProtectedApi(msg.path, msg.payload).then(data => {
|
||||||
|
sendResponse({ ok: true, data });
|
||||||
|
}).catch(err => {
|
||||||
|
sendResponse({ ok: false, error: err.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eBay Parsing Request (from Web App via content script or directly)
|
||||||
|
if (msg?.action === "PARSE_URL" && msg.url) {
|
||||||
|
handleParseRequest(msg.url, sendResponse);
|
||||||
|
return true; // async
|
||||||
|
}
|
||||||
|
|
||||||
|
// eBay Parsing Response (from eBay content script)
|
||||||
|
if (msg?.action === "PARSE_COMPLETE") {
|
||||||
|
handleParseComplete(sender.tab?.id, msg.data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle messages from external web apps (externally_connectable)
|
||||||
|
chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => {
|
||||||
|
if (msg?.action === "PARSE_URL" && msg.url) {
|
||||||
|
handleParseRequest(msg.url, sendResponse);
|
||||||
|
return true; // async
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles eBay URL parsing request
|
||||||
|
* Creates a hidden tab, waits for load, sends parse message to content script
|
||||||
|
*/
|
||||||
|
async function handleParseRequest(url, sendResponse) {
|
||||||
|
try {
|
||||||
|
// Validate URL
|
||||||
|
if (!url || typeof url !== 'string' || !url.toLowerCase().includes('ebay.')) {
|
||||||
|
sendResponse({ ok: false, error: "Invalid eBay URL" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create hidden tab
|
||||||
|
const tab = await chrome.tabs.create({
|
||||||
|
url: url,
|
||||||
|
active: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabId = tab.id;
|
||||||
|
|
||||||
|
// Set up timeout
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
cleanupParseRequest(tabId, null, { ok: false, error: "timeout" });
|
||||||
|
}, PARSE_TIMEOUT_MS);
|
||||||
|
|
||||||
|
// Store request info
|
||||||
|
activeParseRequests.set(tabId, {
|
||||||
|
timeout: timeoutId,
|
||||||
|
sendResponse: sendResponse
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for tab to load, then send parse message
|
||||||
|
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
|
||||||
|
if (updatedTabId !== tabId) return;
|
||||||
|
|
||||||
|
// Tab is fully loaded
|
||||||
|
if (changeInfo.status === 'complete' && updatedTab.url) {
|
||||||
|
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
||||||
|
|
||||||
|
// Small delay to ensure DOM is ready
|
||||||
|
setTimeout(() => {
|
||||||
|
// Send parse message to content script
|
||||||
|
chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" })
|
||||||
|
.then(response => {
|
||||||
|
if (response && response.ok && response.data) {
|
||||||
|
handleParseComplete(tabId, response.data);
|
||||||
|
} else {
|
||||||
|
cleanupParseRequest(tabId, null, { ok: false, error: "Parsing failed" });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Error sending parse message:", err);
|
||||||
|
cleanupParseRequest(tabId, null, { ok: false, error: "Content script error" });
|
||||||
|
});
|
||||||
|
}, 1000); // 1 second delay for DOM ready
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.tabs.onUpdated.addListener(checkTabLoaded);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in handleParseRequest:", error);
|
||||||
|
sendResponse({ ok: false, error: error.message || "Unknown error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles parse complete response from content script
|
||||||
|
*/
|
||||||
|
function handleParseComplete(tabId, data) {
|
||||||
|
cleanupParseRequest(tabId, data, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up parse request: closes tab, clears timeout, sends response
|
||||||
|
*/
|
||||||
|
async function cleanupParseRequest(tabId, data, error) {
|
||||||
|
const request = activeParseRequests.get(tabId);
|
||||||
|
if (!request) return;
|
||||||
|
|
||||||
|
// Clear timeout
|
||||||
|
if (request.timeout) {
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from active requests
|
||||||
|
activeParseRequests.delete(tabId);
|
||||||
|
|
||||||
|
// Close tab
|
||||||
|
try {
|
||||||
|
await chrome.tabs.remove(tabId);
|
||||||
|
} catch (err) {
|
||||||
|
// Tab might already be closed
|
||||||
|
console.warn("Could not close tab:", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send response
|
||||||
|
if (request.sendResponse) {
|
||||||
|
if (error) {
|
||||||
|
request.sendResponse(error);
|
||||||
|
} else if (data) {
|
||||||
|
request.sendResponse({ ok: true, data: data });
|
||||||
|
} else {
|
||||||
|
request.sendResponse({ ok: false, error: "Unknown error" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getJwt() {
|
||||||
|
const data = await chrome.storage.local.get(STORAGE_KEY);
|
||||||
|
return data[STORAGE_KEY] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function callProtectedApi(path, payload) {
|
||||||
|
const jwt = await getJwt();
|
||||||
|
if (!jwt) throw new Error("Not authed");
|
||||||
|
|
||||||
|
const res = await fetch(`${BACKEND_URL}${path}`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
"authorization": `Bearer ${jwt}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload || {})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
// EShip Extension - Background Service Worker
|
|
||||||
// Handles Appwrite authentication and message passing
|
|
||||||
|
|
||||||
console.log('[SW] Service Worker starting...');
|
|
||||||
|
|
||||||
// Define APPWRITE_CONFIG directly (fallback if config.js fails to load)
|
|
||||||
var APPWRITE_CONFIG = {
|
|
||||||
endpoint: 'https://appwrite.webklar.com/v1',
|
|
||||||
projectId: '696b82bb0036d2e547ad',
|
|
||||||
apiKey: 'standard_d48c6eebe825b55e685d8e66ea056161105470702da77b730aca08c106ffbadfa2375ff675dbe9e01d7bb72b4a9fa001ff7c365b73759bc5fb3da432c3cd9cee1151e67517e9838d1f96f942d9891ce66ddc6f11c0fdd67a24f7c84e0fa9999a74dacf2c6aa3533998c177f190fc87ffb5a30b27474be21aece4c70d71d205ba'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Global state
|
|
||||||
let client, account;
|
|
||||||
let scriptsLoaded = false;
|
|
||||||
let initError = null;
|
|
||||||
let DEFAULT_TOOLS = [
|
|
||||||
{
|
|
||||||
id: 'highlight_prices',
|
|
||||||
name: 'Preise hervorheben',
|
|
||||||
enabled: false,
|
|
||||||
settings: {
|
|
||||||
selector: '.price',
|
|
||||||
borderColor: '#ff0000',
|
|
||||||
borderWidth: '2px'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Register message listener FIRST - before any initialization
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|
||||||
console.log('[SW] Message received:', request.action);
|
|
||||||
|
|
||||||
// PING handler - synchronous response
|
|
||||||
if (request.action === 'PING') {
|
|
||||||
console.log('[SW] PING received, responding');
|
|
||||||
sendResponse({ success: true, message: 'Service Worker is alive' });
|
|
||||||
return false; // Synchronous response
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle other messages asynchronously
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
// Ensure scripts are loaded (synchronous operation)
|
|
||||||
if (!scriptsLoaded) {
|
|
||||||
loadScripts();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure Appwrite is initialized
|
|
||||||
if (!client || !account) {
|
|
||||||
initAppwrite();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle the message
|
|
||||||
const response = await handleMessage(request, sender);
|
|
||||||
console.log('[SW] Sending response:', response);
|
|
||||||
sendResponse(response);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[SW] Error handling message:', error);
|
|
||||||
sendResponse({
|
|
||||||
success: false,
|
|
||||||
error: error.message || 'Unbekannter Fehler'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return true; // Keep channel open for async
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load scripts - try relative path first, then fallback to inline
|
|
||||||
function loadScripts() {
|
|
||||||
if (scriptsLoaded) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
console.log('[SW] Loading scripts...');
|
|
||||||
|
|
||||||
// Try loading config first (smaller file) - will override default if successful
|
|
||||||
try {
|
|
||||||
importScripts('../config.js');
|
|
||||||
console.log('[SW] config.js loaded (overriding default)');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[SW] Failed to load config.js:', e);
|
|
||||||
console.log('[SW] Using default inline config');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try loading Appwrite SDK
|
|
||||||
try {
|
|
||||||
importScripts('../lib/appwrite.min.js');
|
|
||||||
console.log('[SW] appwrite.min.js loaded via importScripts');
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[SW] Failed to load appwrite.min.js via importScripts:', e);
|
|
||||||
// Load Appwrite SDK inline
|
|
||||||
loadAppwriteSDKInline();
|
|
||||||
console.log('[SW] appwrite.min.js loaded inline');
|
|
||||||
}
|
|
||||||
|
|
||||||
scriptsLoaded = true;
|
|
||||||
console.log('[SW] Scripts loaded successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[SW] Failed to load scripts:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Appwrite SDK inline if importScripts fails
|
|
||||||
function loadAppwriteSDKInline() {
|
|
||||||
if (typeof Appwrite !== 'undefined') return;
|
|
||||||
|
|
||||||
(function(global) {
|
|
||||||
'use strict';
|
|
||||||
const Appwrite = {};
|
|
||||||
|
|
||||||
class Client {
|
|
||||||
constructor() {
|
|
||||||
this.config = { endpoint: 'https://appwrite.webklar.com/v1', project: '' };
|
|
||||||
this.headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
'x-sdk-name': 'Chrome Extension',
|
|
||||||
'x-sdk-platform': 'client',
|
|
||||||
'x-sdk-language': 'web',
|
|
||||||
'x-sdk-version': '21.0.0',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
setEndpoint(endpoint) { this.config.endpoint = endpoint; return this; }
|
|
||||||
setProject(project) { this.config.project = project; this.headers['x-appwrite-project'] = project; return this; }
|
|
||||||
setKey(key) { if (key) this.headers['x-appwrite-key'] = key; else delete this.headers['x-appwrite-key']; return this; }
|
|
||||||
async call(method, path, headers = {}, params = {}) {
|
|
||||||
const url = new URL(this.config.endpoint + path);
|
|
||||||
const options = { method: method.toUpperCase(), headers: { ...this.headers, ...headers }, credentials: 'include' };
|
|
||||||
if (method === 'GET') {
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (value !== undefined) url.searchParams.append(key, value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.body = JSON.stringify(params);
|
|
||||||
}
|
|
||||||
const response = await fetch(url.toString(), options);
|
|
||||||
const contentType = response.headers.get('content-type') || '';
|
|
||||||
let data = contentType.includes('application/json') ? await response.json() : await response.text();
|
|
||||||
if (response.status >= 400) {
|
|
||||||
throw new AppwriteException(data.message || 'Unknown error', response.status, data.type || '', data);
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Account {
|
|
||||||
constructor(client) { this.client = client; }
|
|
||||||
async get() { return await this.client.call('GET', '/account'); }
|
|
||||||
async createEmailPasswordSession(email, password) {
|
|
||||||
return await this.client.call('POST', '/account/sessions/email', {}, { email, password });
|
|
||||||
}
|
|
||||||
async deleteSession(sessionId) { return await this.client.call('DELETE', `/account/sessions/${sessionId}`); }
|
|
||||||
async getSession(sessionId) { return await this.client.call('GET', `/account/sessions/${sessionId}`); }
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppwriteException extends Error {
|
|
||||||
constructor(message, code = 0, type = '', response = null) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'AppwriteException';
|
|
||||||
this.code = code;
|
|
||||||
this.type = type;
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Appwrite.Client = Client;
|
|
||||||
Appwrite.Account = Account;
|
|
||||||
Appwrite.AppwriteException = AppwriteException;
|
|
||||||
global.Appwrite = Appwrite;
|
|
||||||
})(typeof self !== 'undefined' ? self : this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Appwrite
|
|
||||||
// Note: For user authentication, we don't use API key (only for server operations)
|
|
||||||
function initAppwrite() {
|
|
||||||
try {
|
|
||||||
if (!scriptsLoaded) {
|
|
||||||
throw new Error('Scripts nicht geladen');
|
|
||||||
}
|
|
||||||
if (typeof Appwrite === 'undefined') {
|
|
||||||
throw new Error('Appwrite SDK nicht geladen');
|
|
||||||
}
|
|
||||||
|
|
||||||
// APPWRITE_CONFIG should be available (defined at top of file or loaded from config.js)
|
|
||||||
if (typeof APPWRITE_CONFIG === 'undefined') {
|
|
||||||
throw new Error('APPWRITE_CONFIG nicht definiert');
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = APPWRITE_CONFIG;
|
|
||||||
console.log('[SW] Using config:', {
|
|
||||||
endpoint: config.endpoint,
|
|
||||||
projectId: config.projectId
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize client WITHOUT API key for user authentication
|
|
||||||
// API key is only for server-side operations, not user login
|
|
||||||
client = new Appwrite.Client();
|
|
||||||
client
|
|
||||||
.setEndpoint(config.endpoint)
|
|
||||||
.setProject(config.projectId);
|
|
||||||
// Do NOT set API key here - it conflicts with user sessions
|
|
||||||
|
|
||||||
console.log('[SW] Client configured with project:', config.projectId);
|
|
||||||
console.log('[SW] Note: API Key not set (only for server operations, not user auth)');
|
|
||||||
|
|
||||||
account = new Appwrite.Account(client);
|
|
||||||
initError = null;
|
|
||||||
console.log('[SW] Appwrite Client initialized (session-based, no API key)');
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[SW] Failed to initialize Appwrite:', error);
|
|
||||||
initError = error.message;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle messages
|
|
||||||
async function handleMessage(request, sender) {
|
|
||||||
switch (request.action) {
|
|
||||||
case 'CHECK_AUTH':
|
|
||||||
return await checkAuth();
|
|
||||||
|
|
||||||
case 'LOGIN':
|
|
||||||
return await login(request.email, request.password);
|
|
||||||
|
|
||||||
case 'LOGOUT':
|
|
||||||
return await logout();
|
|
||||||
|
|
||||||
case 'GET_USER':
|
|
||||||
return await getUser();
|
|
||||||
|
|
||||||
case 'GET_SETTINGS':
|
|
||||||
return await getSettings();
|
|
||||||
|
|
||||||
case 'SAVE_SETTINGS':
|
|
||||||
return await saveSettings(request.settings);
|
|
||||||
|
|
||||||
case 'GET_SESSION':
|
|
||||||
return await getSession();
|
|
||||||
|
|
||||||
default:
|
|
||||||
return { success: false, error: 'Unknown action: ' + request.action };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is authenticated
|
|
||||||
async function checkAuth() {
|
|
||||||
try {
|
|
||||||
// Check if we have a stored session
|
|
||||||
const stored = await chrome.storage.local.get(['session']);
|
|
||||||
if (!stored.session || !stored.session.sessionToken) {
|
|
||||||
return { success: true, authenticated: false, user: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify session with API server
|
|
||||||
const API_SERVER_URL = 'http://localhost:3001';
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_SERVER_URL}/api/extension/auth`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'x-session-token': stored.session.sessionToken
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success && data.authenticated) {
|
|
||||||
return { success: true, authenticated: true, user: data.user };
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('[SW] API server check failed:', e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, authenticated: false, user: null };
|
|
||||||
} catch (error) {
|
|
||||||
console.log('[SW] Auth check failed:', error.message);
|
|
||||||
return { success: true, authenticated: false, user: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login with email and password
|
|
||||||
// Uses API server proxy to avoid platform registration issues
|
|
||||||
async function login(email, password) {
|
|
||||||
try {
|
|
||||||
console.log('[SW] Attempting login for:', email);
|
|
||||||
|
|
||||||
// Use API server instead of direct Appwrite call
|
|
||||||
// This avoids the need to register Extension ID as platform
|
|
||||||
const API_SERVER_URL = 'http://localhost:3001';
|
|
||||||
|
|
||||||
const response = await fetch(`${API_SERVER_URL}/api/extension/login`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password })
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!data.success) {
|
|
||||||
throw new Error(data.error || 'Login fehlgeschlagen');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store session info including token
|
|
||||||
await chrome.storage.local.set({
|
|
||||||
session: {
|
|
||||||
userId: data.user.$id,
|
|
||||||
sessionId: data.session.$id,
|
|
||||||
sessionToken: data.session.token || data.session.$id,
|
|
||||||
expire: data.session.expire
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[SW] Login successful for:', data.user.email);
|
|
||||||
|
|
||||||
// Initialize Appwrite client with session for future requests
|
|
||||||
if (!client || !account) {
|
|
||||||
initAppwrite();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, user: data.user, session: data.session };
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[SW] Login error:', error);
|
|
||||||
return { success: false, error: error.message || 'Login fehlgeschlagen' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout
|
|
||||||
async function logout() {
|
|
||||||
try {
|
|
||||||
// Get session token
|
|
||||||
const stored = await chrome.storage.local.get(['session']);
|
|
||||||
const sessionToken = stored.session?.sessionToken;
|
|
||||||
|
|
||||||
if (sessionToken) {
|
|
||||||
// Logout via API server
|
|
||||||
const API_SERVER_URL = 'http://localhost:3001';
|
|
||||||
try {
|
|
||||||
await fetch(`${API_SERVER_URL}/api/extension/logout`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'x-session-token': sessionToken
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await chrome.storage.local.remove(['session']);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
await chrome.storage.local.remove(['session']);
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current user
|
|
||||||
async function getUser() {
|
|
||||||
try {
|
|
||||||
const user = await account.get();
|
|
||||||
return { success: true, user };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get session
|
|
||||||
async function getSession() {
|
|
||||||
try {
|
|
||||||
const session = await account.getSession('current');
|
|
||||||
return { success: true, session };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get tool settings
|
|
||||||
async function getSettings() {
|
|
||||||
try {
|
|
||||||
const result = await chrome.storage.sync.get(['tools']);
|
|
||||||
const tools = result.tools || DEFAULT_TOOLS;
|
|
||||||
return { success: true, tools };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: true, tools: DEFAULT_TOOLS };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save tool settings
|
|
||||||
async function saveSettings(tools) {
|
|
||||||
try {
|
|
||||||
await chrome.storage.sync.set({ tools });
|
|
||||||
|
|
||||||
// Notify content scripts
|
|
||||||
const tabs = await chrome.tabs.query({ url: ['http://localhost:5173/*', 'http://localhost:3000/*'] });
|
|
||||||
for (const tab of tabs) {
|
|
||||||
try {
|
|
||||||
await chrome.tabs.sendMessage(tab.id, { action: 'SETTINGS_UPDATED', tools });
|
|
||||||
} catch (e) {
|
|
||||||
// Tab might not have content script
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on install
|
|
||||||
chrome.runtime.onInstalled.addListener(async (details) => {
|
|
||||||
if (details.reason === 'install') {
|
|
||||||
await chrome.storage.sync.set({ tools: DEFAULT_TOOLS });
|
|
||||||
console.log('[SW] Extension installed');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[SW] Service Worker ready');
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Appwrite Configuration
|
|
||||||
// Update these values with your Appwrite instance details
|
|
||||||
|
|
||||||
// Use var instead of const to ensure global scope in service worker
|
|
||||||
var APPWRITE_CONFIG = {
|
|
||||||
endpoint: 'https://appwrite.webklar.com/v1',
|
|
||||||
projectId: '696b82bb0036d2e547ad',
|
|
||||||
apiKey: 'standard_d48c6eebe825b55e685d8e66ea056161105470702da77b730aca08c106ffbadfa2375ff675dbe9e01d7bb72b4a9fa001ff7c365b73759bc5fb3da432c3cd9cee1151e67517e9838d1f96f942d9891ce66ddc6f11c0fdd67a24f7c84e0fa9999a74dacf2c6aa3533998c177f190fc87ffb5a30b27474be21aece4c70d71d205ba' // Set your API Key here (see README for instructions)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Protected website URL (where the extension will be active)
|
|
||||||
var PROTECTED_SITE_URL = 'http://localhost:5173';
|
|
||||||
|
|
||||||
// Export for use in service worker and content scripts
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
|
||||||
module.exports = { APPWRITE_CONFIG, PROTECTED_SITE_URL };
|
|
||||||
}
|
|
||||||
70
Extension/content-script.js
Normal file
70
Extension/content-script.js
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Content Script läuft auf der Web-App-Seite
|
||||||
|
// Lauscht auf window.postMessage von der Web-App und leitet an Background weiter
|
||||||
|
|
||||||
|
const MESSAGE_SOURCE = "eship-webapp";
|
||||||
|
|
||||||
|
// Markiere Extension als verfügbar
|
||||||
|
// #region agent log
|
||||||
|
try {
|
||||||
|
console.log('[ESHIP-CONTENT] Content script loaded');
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.__EBAY_EXTENSION__ = true;
|
||||||
|
console.log('[ESHIP-CONTENT] window.__EBAY_EXTENSION__ set to true');
|
||||||
|
} else {
|
||||||
|
console.error('[ESHIP-CONTENT] window is undefined!');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ESHIP-CONTENT] Error setting flag:', e);
|
||||||
|
}
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
// Sicherheitscheck: Nur Nachrichten von derselben Origin akzeptieren
|
||||||
|
if (event.data?.source !== MESSAGE_SOURCE) return;
|
||||||
|
|
||||||
|
// Auth Messages (JWT)
|
||||||
|
if (event.data.type === "AUTH_JWT" || event.data.type === "AUTH_CLEARED") {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{
|
||||||
|
type: event.data.type,
|
||||||
|
jwt: event.data.jwt,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
// Antwort zurück an Web-App senden
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: "eship-extension",
|
||||||
|
type: event.data.type,
|
||||||
|
response: response,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eBay Parsing Request (PARSE_URL)
|
||||||
|
if (event.data.action === "PARSE_URL" && event.data.url) {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
{
|
||||||
|
action: "PARSE_URL",
|
||||||
|
url: event.data.url,
|
||||||
|
},
|
||||||
|
(response) => {
|
||||||
|
// Antwort zurück an Web-App senden
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
source: "eship-extension",
|
||||||
|
messageId: event.data.messageId,
|
||||||
|
ok: response?.ok,
|
||||||
|
data: response?.data,
|
||||||
|
error: response?.error,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
// EShip Extension Content Script
|
|
||||||
// Injects into protected website to enforce auth and apply tools
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// State
|
|
||||||
let isAuthenticated = false;
|
|
||||||
let currentTools = [];
|
|
||||||
let blockedOverlay = null;
|
|
||||||
|
|
||||||
// Initialize on page load
|
|
||||||
init();
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
// Check auth status immediately
|
|
||||||
const authResponse = await sendMessage({ action: 'CHECK_AUTH' });
|
|
||||||
|
|
||||||
if (authResponse.success && authResponse.authenticated) {
|
|
||||||
isAuthenticated = true;
|
|
||||||
await loadAndApplyTools();
|
|
||||||
} else {
|
|
||||||
isAuthenticated = false;
|
|
||||||
showBlockedScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for messages from service worker (settings updates)
|
|
||||||
chrome.runtime.onMessage.addListener(handleMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message to service worker
|
|
||||||
function sendMessage(message) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
chrome.runtime.sendMessage(message, (response) => {
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
resolve({ success: false, error: chrome.runtime.lastError.message });
|
|
||||||
} else {
|
|
||||||
resolve(response || { success: false, error: 'No response' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle incoming messages
|
|
||||||
function handleMessage(request, sender, sendResponse) {
|
|
||||||
switch (request.action) {
|
|
||||||
case 'SETTINGS_UPDATED':
|
|
||||||
currentTools = request.tools;
|
|
||||||
applyTools();
|
|
||||||
sendResponse({ success: true });
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'AUTH_CHANGED':
|
|
||||||
if (request.authenticated) {
|
|
||||||
isAuthenticated = true;
|
|
||||||
hideBlockedScreen();
|
|
||||||
loadAndApplyTools();
|
|
||||||
} else {
|
|
||||||
isAuthenticated = false;
|
|
||||||
showBlockedScreen();
|
|
||||||
}
|
|
||||||
sendResponse({ success: true });
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show blocked screen when not authenticated
|
|
||||||
function showBlockedScreen() {
|
|
||||||
// Wait for DOM to be ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', createBlockedOverlay);
|
|
||||||
} else {
|
|
||||||
createBlockedOverlay();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBlockedOverlay() {
|
|
||||||
// Remove existing overlay if any
|
|
||||||
hideBlockedScreen();
|
|
||||||
|
|
||||||
// Create overlay
|
|
||||||
blockedOverlay = document.createElement('div');
|
|
||||||
blockedOverlay.id = 'eship-blocked-overlay';
|
|
||||||
blockedOverlay.innerHTML = `
|
|
||||||
<style>
|
|
||||||
#eship-blocked-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 999999;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay .blocked-icon {
|
|
||||||
font-size: 64px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay h1 {
|
|
||||||
color: #FD366E;
|
|
||||||
font-size: 28px;
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay p {
|
|
||||||
color: #9ca3af;
|
|
||||||
font-size: 16px;
|
|
||||||
margin: 0 0 32px 0;
|
|
||||||
text-align: center;
|
|
||||||
max-width: 400px;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay .instructions {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay .instructions h2 {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0 0 16px 0;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay .instructions ol {
|
|
||||||
color: #d1d5db;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
line-height: 2;
|
|
||||||
}
|
|
||||||
#eship-blocked-overlay .instructions li {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<div class="blocked-icon">🔒</div>
|
|
||||||
<h1>Zugriff gesperrt</h1>
|
|
||||||
<p>Diese Website ist geschuetzt und erfordert eine Authentifizierung ueber die EShip Browser-Extension.</p>
|
|
||||||
<div class="instructions">
|
|
||||||
<h2>So melden Sie sich an:</h2>
|
|
||||||
<ol>
|
|
||||||
<li>Klicken Sie auf das EShip-Icon in der Browser-Toolbar</li>
|
|
||||||
<li>Geben Sie Ihre E-Mail und Passwort ein</li>
|
|
||||||
<li>Klicken Sie auf "Anmelden"</li>
|
|
||||||
<li>Die Seite wird automatisch freigeschaltet</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.body.appendChild(blockedOverlay);
|
|
||||||
|
|
||||||
// Prevent scrolling on body
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide blocked screen
|
|
||||||
function hideBlockedScreen() {
|
|
||||||
if (blockedOverlay) {
|
|
||||||
blockedOverlay.remove();
|
|
||||||
blockedOverlay = null;
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load and apply tool settings
|
|
||||||
async function loadAndApplyTools() {
|
|
||||||
const response = await sendMessage({ action: 'GET_SETTINGS' });
|
|
||||||
if (response.success) {
|
|
||||||
currentTools = response.tools;
|
|
||||||
applyTools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply all enabled tools
|
|
||||||
function applyTools() {
|
|
||||||
if (!isAuthenticated) return;
|
|
||||||
|
|
||||||
// Wait for DOM to be ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => executeTools());
|
|
||||||
} else {
|
|
||||||
executeTools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function executeTools() {
|
|
||||||
currentTools.forEach(tool => {
|
|
||||||
if (tool.enabled) {
|
|
||||||
applyTool(tool);
|
|
||||||
} else {
|
|
||||||
removeTool(tool);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply a specific tool
|
|
||||||
function applyTool(tool) {
|
|
||||||
switch (tool.id) {
|
|
||||||
case 'highlight_prices':
|
|
||||||
applyHighlightPrices(tool.settings);
|
|
||||||
break;
|
|
||||||
// Add more tools here as needed
|
|
||||||
default:
|
|
||||||
console.log('[EShip] Unknown tool:', tool.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a specific tool's effects
|
|
||||||
function removeTool(tool) {
|
|
||||||
switch (tool.id) {
|
|
||||||
case 'highlight_prices':
|
|
||||||
removeHighlightPrices(tool.settings);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// TOOL: Highlight Prices
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
function applyHighlightPrices(settings) {
|
|
||||||
const selector = settings.selector || '.price';
|
|
||||||
const borderColor = settings.borderColor || '#ff0000';
|
|
||||||
const borderWidth = settings.borderWidth || '2px';
|
|
||||||
|
|
||||||
// Add custom style if not exists
|
|
||||||
let styleEl = document.getElementById('eship-highlight-prices-style');
|
|
||||||
if (!styleEl) {
|
|
||||||
styleEl = document.createElement('style');
|
|
||||||
styleEl.id = 'eship-highlight-prices-style';
|
|
||||||
document.head.appendChild(styleEl);
|
|
||||||
}
|
|
||||||
|
|
||||||
styleEl.textContent = `
|
|
||||||
${selector} {
|
|
||||||
border: ${borderWidth} solid ${borderColor} !important;
|
|
||||||
box-shadow: 0 0 8px ${borderColor}40 !important;
|
|
||||||
border-radius: 4px !important;
|
|
||||||
padding: 2px 4px !important;
|
|
||||||
transition: all 0.2s ease !important;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Also add data attribute to track highlighted elements
|
|
||||||
document.querySelectorAll(selector).forEach(el => {
|
|
||||||
el.dataset.eshipHighlighted = 'true';
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[EShip] Highlight Prices applied with selector:', selector);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeHighlightPrices(settings) {
|
|
||||||
// Remove style element
|
|
||||||
const styleEl = document.getElementById('eship-highlight-prices-style');
|
|
||||||
if (styleEl) {
|
|
||||||
styleEl.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove data attributes
|
|
||||||
document.querySelectorAll('[data-eship-highlighted]').forEach(el => {
|
|
||||||
delete el.dataset.eshipHighlighted;
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[EShip] Highlight Prices removed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// Mutation Observer for dynamic content
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Re-apply tools when new content is added
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
if (!isAuthenticated) return;
|
|
||||||
|
|
||||||
let shouldReapply = false;
|
|
||||||
mutations.forEach(mutation => {
|
|
||||||
if (mutation.addedNodes.length > 0) {
|
|
||||||
shouldReapply = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldReapply) {
|
|
||||||
// Debounce reapplication
|
|
||||||
clearTimeout(observer.timeout);
|
|
||||||
observer.timeout = setTimeout(() => {
|
|
||||||
applyTools();
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start observing once DOM is ready
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
})();
|
|
||||||
361
Extension/ebay-content-script.js
Normal file
361
Extension/ebay-content-script.js
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* eBay Content Script
|
||||||
|
* Wird auf eBay-Seiten ausgeführt und extrahiert Verkäufer-/Shop-Daten aus dem DOM
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Message Listener für Parsing-Anfragen
|
||||||
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
if (message.action === "PARSE_EBAY") {
|
||||||
|
try {
|
||||||
|
const parsedData = parseEbayPage();
|
||||||
|
sendResponse({ ok: true, data: parsedData });
|
||||||
|
} catch (error) {
|
||||||
|
// Niemals unhandled throws - immer graceful response
|
||||||
|
sendResponse({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
sellerId: "",
|
||||||
|
shopName: "",
|
||||||
|
market: extractMarketFromHostname(),
|
||||||
|
status: "unknown",
|
||||||
|
stats: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return true; // async response
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert Marktplatz aus hostname
|
||||||
|
* @returns {string} Market Code (z.B. "DE", "US", "UK")
|
||||||
|
*/
|
||||||
|
function extractMarketFromHostname() {
|
||||||
|
try {
|
||||||
|
const hostname = window.location.hostname.toLowerCase();
|
||||||
|
|
||||||
|
// eBay Domain-Patterns
|
||||||
|
if (hostname.includes('.de') || hostname.includes('ebay.de')) {
|
||||||
|
return 'DE';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.com') && !hostname.includes('.uk')) {
|
||||||
|
return 'US';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.uk') || hostname.includes('ebay.co.uk')) {
|
||||||
|
return 'UK';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.fr') || hostname.includes('ebay.fr')) {
|
||||||
|
return 'FR';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.it') || hostname.includes('ebay.it')) {
|
||||||
|
return 'IT';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.es') || hostname.includes('ebay.es')) {
|
||||||
|
return 'ES';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.nl') || hostname.includes('ebay.nl')) {
|
||||||
|
return 'NL';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.at') || hostname.includes('ebay.at')) {
|
||||||
|
return 'AT';
|
||||||
|
}
|
||||||
|
if (hostname.includes('.ch') || hostname.includes('ebay.ch')) {
|
||||||
|
return 'CH';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: erster Teil der Domain nach "ebay."
|
||||||
|
const match = hostname.match(/ebay\.([a-z]{2,3})/);
|
||||||
|
if (match && match[1]) {
|
||||||
|
return match[1].toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'US'; // Default
|
||||||
|
} catch (e) {
|
||||||
|
return 'US';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert Seller ID aus URL oder DOM
|
||||||
|
* @returns {string} Seller ID
|
||||||
|
*/
|
||||||
|
function extractSellerId() {
|
||||||
|
try {
|
||||||
|
// Methode 1: URL-Patterns
|
||||||
|
const url = window.location.href;
|
||||||
|
const urlLower = url.toLowerCase();
|
||||||
|
|
||||||
|
// Pattern: /usr/username oder /str/storename
|
||||||
|
const usrMatch = url.match(/\/usr\/([^\/\?]+)/i);
|
||||||
|
if (usrMatch && usrMatch[1]) {
|
||||||
|
return usrMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const strMatch = url.match(/\/str\/([^\/\?]+)/i);
|
||||||
|
if (strMatch && strMatch[1]) {
|
||||||
|
return strMatch[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methode 2: DOM-Elemente suchen
|
||||||
|
// Suche nach verschiedenen Selektoren, die Seller-ID enthalten könnten
|
||||||
|
const possibleSelectors = [
|
||||||
|
'[data-testid*="seller"]',
|
||||||
|
'.seller-username',
|
||||||
|
'.member-info-username',
|
||||||
|
'[class*="seller-id"]',
|
||||||
|
'[class*="username"]',
|
||||||
|
'[id*="seller"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of possibleSelectors) {
|
||||||
|
try {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
const text = element.textContent?.trim();
|
||||||
|
if (text && text.length > 0 && text.length < 100) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methode 3: Meta-Tags
|
||||||
|
try {
|
||||||
|
const metaSeller = document.querySelector('meta[property*="seller"], meta[name*="seller"]');
|
||||||
|
if (metaSeller && metaSeller.content) {
|
||||||
|
return metaSeller.content.trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""; // Nicht gefunden
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert Shop Name aus DOM
|
||||||
|
* @returns {string} Shop Name
|
||||||
|
*/
|
||||||
|
function extractShopName() {
|
||||||
|
try {
|
||||||
|
// Methode 1: Spezifische Selektoren versuchen
|
||||||
|
const shopNameSelectors = [
|
||||||
|
'h1.shop-name',
|
||||||
|
'.store-name',
|
||||||
|
'[data-testid="store-name"]',
|
||||||
|
'.store-title',
|
||||||
|
'[class*="shop-name"]',
|
||||||
|
'[class*="store-title"]',
|
||||||
|
'h1[class*="store"]',
|
||||||
|
'h1[class*="shop"]'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of shopNameSelectors) {
|
||||||
|
try {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
const text = element.textContent?.trim();
|
||||||
|
if (text && text.length > 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue to next selector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methode 2: document.title parsen
|
||||||
|
try {
|
||||||
|
const title = document.title || "";
|
||||||
|
// Versuche Muster wie "Shop Name | eBay" zu extrahieren
|
||||||
|
const titleMatch = title.match(/^(.+?)\s*[\|\-]\s*eBay/i);
|
||||||
|
if (titleMatch && titleMatch[1]) {
|
||||||
|
return titleMatch[1].trim();
|
||||||
|
}
|
||||||
|
// Wenn Titel einfach genug ist, verwende ihn direkt
|
||||||
|
if (title && title.length < 100 && !title.toLowerCase().includes('ebay')) {
|
||||||
|
return title.trim();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methode 3: h1 Tag als Fallback
|
||||||
|
try {
|
||||||
|
const h1 = document.querySelector('h1');
|
||||||
|
if (h1) {
|
||||||
|
const text = h1.textContent?.trim();
|
||||||
|
if (text && text.length > 0 && text.length < 200) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""; // Nicht gefunden
|
||||||
|
} catch (e) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert Stats aus DOM (Feedback Score, Positive Rate, etc.)
|
||||||
|
* @returns {object} Stats Objekt
|
||||||
|
*/
|
||||||
|
function extractStats() {
|
||||||
|
const stats = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pageText = document.body?.textContent || "";
|
||||||
|
|
||||||
|
// Feedback Score
|
||||||
|
try {
|
||||||
|
const feedbackScorePatterns = [
|
||||||
|
/feedback\s*score[:\s]+(\d+)/i,
|
||||||
|
/(?:score|bewertung)[:\s]+(\d+)/i,
|
||||||
|
/(\d+)\s*feedback/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of feedbackScorePatterns) {
|
||||||
|
const match = pageText.match(pattern);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const score = parseInt(match[1], 10);
|
||||||
|
if (!isNaN(score) && score >= 0) {
|
||||||
|
stats.feedbackScore = score;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positive Rate (%)
|
||||||
|
try {
|
||||||
|
const positiveRatePatterns = [
|
||||||
|
/positive[:\s]+(\d+\.?\d*)\s*%/i,
|
||||||
|
/(\d+\.?\d*)\s*%\s*positive/i,
|
||||||
|
/(?:rate|quote)[:\s]+(\d+\.?\d*)\s*%/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of positiveRatePatterns) {
|
||||||
|
const match = pageText.match(pattern);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const rate = parseFloat(match[1]);
|
||||||
|
if (!isNaN(rate) && rate >= 0 && rate <= 100) {
|
||||||
|
stats.positiveRate = rate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feedback Count
|
||||||
|
try {
|
||||||
|
const feedbackCountPatterns = [
|
||||||
|
/(\d+)\s*(?:bewertungen|ratings|feedbacks?)/i,
|
||||||
|
/(?:count|anzahl)[:\s]+(\d+)/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of feedbackCountPatterns) {
|
||||||
|
const match = pageText.match(pattern);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const count = parseInt(match[1], 10);
|
||||||
|
if (!isNaN(count) && count >= 0) {
|
||||||
|
stats.feedbackCount = count;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Items for Sale
|
||||||
|
try {
|
||||||
|
const itemsPatterns = [
|
||||||
|
/items?\s*(?:for\s*)?sale[:\s]+(\d+)/i,
|
||||||
|
/(\d+)\s*artikel/i,
|
||||||
|
/(\d+)\s*(?:items?|artikel)\s*(?:for\s*)?sale/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of itemsPatterns) {
|
||||||
|
const match = pageText.match(pattern);
|
||||||
|
if (match && match[1]) {
|
||||||
|
const items = parseInt(match[1], 10);
|
||||||
|
if (!isNaN(items) && items >= 0) {
|
||||||
|
stats.itemsForSale = items;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Member Since
|
||||||
|
try {
|
||||||
|
const memberSincePatterns = [
|
||||||
|
/member\s*since[:\s]+(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/i,
|
||||||
|
/mitglied\s*seit[:\s]+(\d{1,2}[\/\-\.]\d{1,2}[\/\-\.]\d{2,4})/i,
|
||||||
|
/(?:since|seit)[:\s]+(\d{4})/i
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of memberSincePatterns) {
|
||||||
|
const match = pageText.match(pattern);
|
||||||
|
if (match && match[1]) {
|
||||||
|
stats.memberSince = match[1].trim();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
// Return empty stats object
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haupt-Parsing-Funktion
|
||||||
|
* @returns {object} Parsed eBay Account Data
|
||||||
|
*/
|
||||||
|
function parseEbayPage() {
|
||||||
|
try {
|
||||||
|
const sellerId = extractSellerId();
|
||||||
|
const shopName = extractShopName();
|
||||||
|
const market = extractMarketFromHostname();
|
||||||
|
const stats = extractStats();
|
||||||
|
|
||||||
|
// Status bestimmen: "active" wenn wir mindestens Shop Name oder Seller ID haben
|
||||||
|
const status = (sellerId || shopName) ? "active" : "unknown";
|
||||||
|
|
||||||
|
return {
|
||||||
|
sellerId: sellerId || "",
|
||||||
|
shopName: shopName || "",
|
||||||
|
market: market,
|
||||||
|
status: status,
|
||||||
|
stats: stats
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// Graceful fallback
|
||||||
|
return {
|
||||||
|
sellerId: "",
|
||||||
|
shopName: "",
|
||||||
|
market: extractMarketFromHostname(),
|
||||||
|
status: "unknown",
|
||||||
|
stats: {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
209
Extension/lib/appwrite.min.js
vendored
209
Extension/lib/appwrite.min.js
vendored
@@ -1,209 +0,0 @@
|
|||||||
/**
|
|
||||||
* Appwrite Web SDK - Minimal Bundle for Chrome Extension
|
|
||||||
* This is a simplified version for extension use
|
|
||||||
* For full SDK, download from: https://cdn.jsdelivr.net/npm/appwrite@latest/dist/iife/sdk.min.js
|
|
||||||
*
|
|
||||||
* Version: 21.x compatible
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(global) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// Appwrite namespace
|
|
||||||
const Appwrite = {};
|
|
||||||
|
|
||||||
// Client class
|
|
||||||
class Client {
|
|
||||||
constructor() {
|
|
||||||
this.config = {
|
|
||||||
endpoint: 'https://cloud.appwrite.io/v1',
|
|
||||||
project: '',
|
|
||||||
};
|
|
||||||
this.headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
'x-sdk-name': 'Chrome Extension',
|
|
||||||
'x-sdk-platform': 'client',
|
|
||||||
'x-sdk-language': 'web',
|
|
||||||
'x-sdk-version': '21.0.0',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setEndpoint(endpoint) {
|
|
||||||
this.config.endpoint = endpoint;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setProject(project) {
|
|
||||||
this.config.project = project;
|
|
||||||
this.headers['x-appwrite-project'] = project;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
setKey(key) {
|
|
||||||
if (key) {
|
|
||||||
this.headers['x-appwrite-key'] = key;
|
|
||||||
} else {
|
|
||||||
delete this.headers['x-appwrite-key'];
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
async call(method, path, headers = {}, params = {}) {
|
|
||||||
const url = new URL(this.config.endpoint + path);
|
|
||||||
const options = {
|
|
||||||
method: method.toUpperCase(),
|
|
||||||
headers: { ...this.headers, ...headers },
|
|
||||||
credentials: 'include',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (method === 'GET') {
|
|
||||||
for (const [key, value] of Object.entries(params)) {
|
|
||||||
if (value !== undefined) {
|
|
||||||
url.searchParams.append(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.body = JSON.stringify(params);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), options);
|
|
||||||
const contentType = response.headers.get('content-type') || '';
|
|
||||||
|
|
||||||
let data;
|
|
||||||
if (contentType.includes('application/json')) {
|
|
||||||
data = await response.json();
|
|
||||||
} else {
|
|
||||||
data = await response.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 400) {
|
|
||||||
throw new AppwriteException(
|
|
||||||
data.message || 'Unknown error',
|
|
||||||
response.status,
|
|
||||||
data.type || '',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Account class
|
|
||||||
class Account {
|
|
||||||
constructor(client) {
|
|
||||||
this.client = client;
|
|
||||||
}
|
|
||||||
|
|
||||||
async get() {
|
|
||||||
return await this.client.call('GET', '/account');
|
|
||||||
}
|
|
||||||
|
|
||||||
async create(userId, email, password, name = undefined) {
|
|
||||||
const params = { userId, email, password };
|
|
||||||
if (name) params.name = name;
|
|
||||||
return await this.client.call('POST', '/account', {}, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async createEmailPasswordSession(email, password) {
|
|
||||||
return await this.client.call('POST', '/account/sessions/email', {}, {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createSession(userId, secret) {
|
|
||||||
return await this.client.call('POST', '/account/sessions', {}, {
|
|
||||||
userId,
|
|
||||||
secret,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getSession(sessionId) {
|
|
||||||
return await this.client.call('GET', `/account/sessions/${sessionId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async listSessions() {
|
|
||||||
return await this.client.call('GET', '/account/sessions');
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSession(sessionId) {
|
|
||||||
return await this.client.call('DELETE', `/account/sessions/${sessionId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteSessions() {
|
|
||||||
return await this.client.call('DELETE', '/account/sessions');
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateEmail(email, password) {
|
|
||||||
return await this.client.call('PATCH', '/account/email', {}, {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updatePassword(password, oldPassword = undefined) {
|
|
||||||
const params = { password };
|
|
||||||
if (oldPassword) params.oldPassword = oldPassword;
|
|
||||||
return await this.client.call('PATCH', '/account/password', {}, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateName(name) {
|
|
||||||
return await this.client.call('PATCH', '/account/name', {}, { name });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPrefs() {
|
|
||||||
return await this.client.call('GET', '/account/prefs');
|
|
||||||
}
|
|
||||||
|
|
||||||
async updatePrefs(prefs) {
|
|
||||||
return await this.client.call('PATCH', '/account/prefs', {}, { prefs });
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRecovery(email, url) {
|
|
||||||
return await this.client.call('POST', '/account/recovery', {}, {
|
|
||||||
email,
|
|
||||||
url,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateRecovery(userId, secret, password) {
|
|
||||||
return await this.client.call('PUT', '/account/recovery', {}, {
|
|
||||||
userId,
|
|
||||||
secret,
|
|
||||||
password,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async createVerification(url) {
|
|
||||||
return await this.client.call('POST', '/account/verification', {}, { url });
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateVerification(userId, secret) {
|
|
||||||
return await this.client.call('PUT', '/account/verification', {}, {
|
|
||||||
userId,
|
|
||||||
secret,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception class
|
|
||||||
class AppwriteException extends Error {
|
|
||||||
constructor(message, code = 0, type = '', response = null) {
|
|
||||||
super(message);
|
|
||||||
this.name = 'AppwriteException';
|
|
||||||
this.code = code;
|
|
||||||
this.type = type;
|
|
||||||
this.response = response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export to Appwrite namespace
|
|
||||||
Appwrite.Client = Client;
|
|
||||||
Appwrite.Account = Account;
|
|
||||||
Appwrite.AppwriteException = AppwriteException;
|
|
||||||
|
|
||||||
// Make available globally
|
|
||||||
global.Appwrite = Appwrite;
|
|
||||||
|
|
||||||
})(typeof self !== 'undefined' ? self : this);
|
|
||||||
@@ -1,44 +1,49 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "EShip Auth Extension",
|
"name": "Protected Extension",
|
||||||
"version": "1.0.0",
|
"version": "0.1.0",
|
||||||
"description": "Authentication extension for EShip protected web app",
|
"action": { "default_popup": "popup.html" },
|
||||||
|
"background": { "service_worker": "background.js", "type": "module" },
|
||||||
"permissions": [
|
|
||||||
"storage",
|
|
||||||
"activeTab",
|
|
||||||
"tabs"
|
|
||||||
],
|
|
||||||
|
|
||||||
"host_permissions": [
|
|
||||||
"http://localhost:5173/*",
|
|
||||||
"http://localhost:3000/*",
|
|
||||||
"http://localhost:3001/*",
|
|
||||||
"https://appwrite.webklar.com/*"
|
|
||||||
],
|
|
||||||
|
|
||||||
"background": {
|
|
||||||
"service_worker": "background/service-worker.js"
|
|
||||||
},
|
|
||||||
|
|
||||||
"action": {
|
|
||||||
"default_popup": "popup/popup.html",
|
|
||||||
"default_title": "EShip Login"
|
|
||||||
},
|
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
"matches": ["http://localhost:5173/*", "http://localhost:3000/*"],
|
"matches": ["http://localhost:*/*", "https://*/*"],
|
||||||
"js": ["content/content-script.js"],
|
"js": ["content-script.js"],
|
||||||
"run_at": "document_start"
|
"run_at": "document_idle"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matches": [
|
||||||
|
"*://*.ebay.de/*",
|
||||||
|
"*://*.ebay.com/*",
|
||||||
|
"*://*.ebay.co.uk/*",
|
||||||
|
"*://*.ebay.fr/*",
|
||||||
|
"*://*.ebay.it/*",
|
||||||
|
"*://*.ebay.es/*",
|
||||||
|
"*://*.ebay.nl/*",
|
||||||
|
"*://*.ebay.at/*",
|
||||||
|
"*://*.ebay.ch/*",
|
||||||
|
"*://*.ebay.com.au/*",
|
||||||
|
"*://*.ebay.ca/*"
|
||||||
|
],
|
||||||
|
"js": ["ebay-content-script.js"],
|
||||||
|
"run_at": "document_idle"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"permissions": ["tabs", "scripting", "storage"],
|
||||||
|
"host_permissions": [
|
||||||
"web_accessible_resources": [
|
"https://*/*",
|
||||||
{
|
"*://*.ebay.de/*",
|
||||||
"resources": ["lib/*", "config.js"],
|
"*://*.ebay.com/*",
|
||||||
"matches": ["http://localhost:5173/*", "http://localhost:3000/*"]
|
"*://*.ebay.co.uk/*",
|
||||||
}
|
"*://*.ebay.fr/*",
|
||||||
]
|
"*://*.ebay.it/*",
|
||||||
|
"*://*.ebay.es/*",
|
||||||
|
"*://*.ebay.nl/*",
|
||||||
|
"*://*.ebay.at/*",
|
||||||
|
"*://*.ebay.ch/*",
|
||||||
|
"*://*.ebay.com.au/*",
|
||||||
|
"*://*.ebay.ca/*"
|
||||||
|
],
|
||||||
|
"externally_connectable": {
|
||||||
|
"matches": ["http://localhost:*/*", "https://*/*"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Extension/popup.html
Normal file
8
Extension/popup.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head><meta charset="utf-8" /></head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
44
Extension/popup.js
Normal file
44
Extension/popup.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
async function getJwt() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.runtime.sendMessage({ type: "GET_JWT" }, (response) => {
|
||||||
|
resolve(response?.jwt || "");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callProtectedApi(path, payload) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
chrome.runtime.sendMessage({ type: "CALL_API", path, payload }, (response) => {
|
||||||
|
if (response?.ok) {
|
||||||
|
resolve(response.data);
|
||||||
|
} else {
|
||||||
|
reject(new Error(response?.error || "API call failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
const jwt = await getJwt();
|
||||||
|
|
||||||
|
root.innerHTML = `
|
||||||
|
<div style="font-family: Arial; padding: 12px; width: 260px;">
|
||||||
|
<div style="font-weight: 800;">Extension</div>
|
||||||
|
<div style="margin-top: 8px;">Authed: ${jwt ? "true" : "false"}</div>
|
||||||
|
<button id="btn" style="margin-top: 10px; padding: 8px 12px; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background: #f0f0f0;">Test action</button>
|
||||||
|
<div id="out" style="margin-top: 10px; font-size: 12px; word-break: break-all;"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.getElementById("btn").onclick = async () => {
|
||||||
|
const out = document.getElementById("out");
|
||||||
|
try {
|
||||||
|
const data = await callProtectedApi("/api/action", { ping: true });
|
||||||
|
out.textContent = JSON.stringify(data);
|
||||||
|
} catch (e) {
|
||||||
|
out.textContent = String(e.message || e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
main();
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
/* EShip Extension Popup Styles */
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #2D2D31;
|
|
||||||
background: #FAFAFB;
|
|
||||||
min-width: 320px;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
padding-bottom: 12px;
|
|
||||||
border-bottom: 1px solid #EDEDF0;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #FD366E;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* States */
|
|
||||||
.state {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.state.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading */
|
|
||||||
#loading {
|
|
||||||
text-align: center;
|
|
||||||
padding: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border: 3px solid #EDEDF0;
|
|
||||||
border-top-color: #FD366E;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
margin: 0 auto 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form */
|
|
||||||
h2 {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #97979B;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #EDEDF0;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
background: #fff;
|
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #FD366E;
|
|
||||||
box-shadow: 0 0 0 3px rgba(253, 54, 110, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Error Message */
|
|
||||||
.error {
|
|
||||||
background: #FEE2E2;
|
|
||||||
color: #B91C1C;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s, transform 0.1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
width: 100%;
|
|
||||||
background: #FD366E;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: #E8305F;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:disabled {
|
|
||||||
background: #FDA4B8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background: #EDEDF0;
|
|
||||||
color: #2D2D31;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background: #D8D8DB;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-small {
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
||||||
border-top-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-spinner.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* User Info */
|
|
||||||
.user-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #EDEDF0;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-icon {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-email {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #97979B;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tools List */
|
|
||||||
#tools-list {
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #EDEDF0;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-item {
|
|
||||||
padding: 12px;
|
|
||||||
border-bottom: 1px solid #EDEDF0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-item:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-name {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle Switch */
|
|
||||||
.toggle {
|
|
||||||
position: relative;
|
|
||||||
width: 44px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle input {
|
|
||||||
opacity: 0;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-slider {
|
|
||||||
position: absolute;
|
|
||||||
cursor: pointer;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: #D8D8DB;
|
|
||||||
transition: 0.3s;
|
|
||||||
border-radius: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-slider:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
left: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
background-color: white;
|
|
||||||
transition: 0.3s;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle input:checked + .toggle-slider {
|
|
||||||
background-color: #FD366E;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle input:checked + .toggle-slider:before {
|
|
||||||
transform: translateX(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tool Settings */
|
|
||||||
.tool-settings {
|
|
||||||
display: none;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool-settings.visible {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row label {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #97979B;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row input {
|
|
||||||
flex: 1;
|
|
||||||
padding: 6px 8px;
|
|
||||||
border: 1px solid #EDEDF0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-row input:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #FD366E;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Actions */
|
|
||||||
.actions {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=400, initial-scale=1.0">
|
|
||||||
<title>EShip Extension</title>
|
|
||||||
<link rel="stylesheet" href="popup.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<header>
|
|
||||||
<h1>EShip</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div id="loading" class="state">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<p>Laden...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Login Form (logged out state) -->
|
|
||||||
<div id="login-form" class="state hidden">
|
|
||||||
<h2>Anmelden</h2>
|
|
||||||
<form id="auth-form">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="email">E-Mail</label>
|
|
||||||
<input type="email" id="email" name="email" required autocomplete="email">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="password">Passwort</label>
|
|
||||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
|
||||||
</div>
|
|
||||||
<div id="error-message" class="error hidden"></div>
|
|
||||||
<button type="submit" id="login-btn" class="btn btn-primary">
|
|
||||||
<span class="btn-text">Anmelden</span>
|
|
||||||
<span class="btn-spinner hidden"></span>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tools Menu (logged in state) -->
|
|
||||||
<div id="tools-menu" class="state hidden">
|
|
||||||
<div class="user-info">
|
|
||||||
<span class="user-icon">👤</span>
|
|
||||||
<span id="user-email"></span>
|
|
||||||
<button id="logout-btn" class="btn btn-small btn-secondary">Abmelden</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Tools</h2>
|
|
||||||
<div id="tools-list">
|
|
||||||
<!-- Tools will be rendered here dynamically -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button id="open-site-btn" class="btn btn-primary">Website oeffnen</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="popup.js" type="module"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
// EShip Extension Popup Logic
|
|
||||||
|
|
||||||
// DOM Elements
|
|
||||||
const loadingEl = document.getElementById('loading');
|
|
||||||
const loginFormEl = document.getElementById('login-form');
|
|
||||||
const toolsMenuEl = document.getElementById('tools-menu');
|
|
||||||
const authForm = document.getElementById('auth-form');
|
|
||||||
const emailInput = document.getElementById('email');
|
|
||||||
const passwordInput = document.getElementById('password');
|
|
||||||
const errorMessage = document.getElementById('error-message');
|
|
||||||
const loginBtn = document.getElementById('login-btn');
|
|
||||||
const logoutBtn = document.getElementById('logout-btn');
|
|
||||||
const userEmailEl = document.getElementById('user-email');
|
|
||||||
const toolsListEl = document.getElementById('tools-list');
|
|
||||||
const openSiteBtn = document.getElementById('open-site-btn');
|
|
||||||
|
|
||||||
// Protected site URL (should match config.js)
|
|
||||||
const PROTECTED_SITE_URL = 'http://localhost:5173';
|
|
||||||
|
|
||||||
// State
|
|
||||||
let currentUser = null;
|
|
||||||
let tools = [];
|
|
||||||
|
|
||||||
// Initialize popup
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
showState('loading');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if service worker is available
|
|
||||||
if (!chrome.runtime || !chrome.runtime.id) {
|
|
||||||
throw new Error('Chrome Runtime nicht verfuegbar');
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, test if service worker is alive
|
|
||||||
console.log('Testing service worker...');
|
|
||||||
const pingResponse = await sendMessage({ action: 'PING' });
|
|
||||||
console.log('PING response:', pingResponse);
|
|
||||||
|
|
||||||
if (!pingResponse) {
|
|
||||||
throw new Error('Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pingResponse.success) {
|
|
||||||
throw new Error('Service Worker Fehler: ' + (pingResponse.error || 'Unbekannter Fehler'));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Service Worker ist aktiv!');
|
|
||||||
|
|
||||||
console.log('Checking auth status...');
|
|
||||||
|
|
||||||
// Check current auth status
|
|
||||||
const response = await sendMessage({ action: 'CHECK_AUTH' });
|
|
||||||
|
|
||||||
if (!response) {
|
|
||||||
throw new Error('Keine Antwort vom Service Worker');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Auth response:', response);
|
|
||||||
|
|
||||||
// If there's an error, show it but still allow login
|
|
||||||
if (response.error && !response.success) {
|
|
||||||
console.error('Service Worker Error:', response.error);
|
|
||||||
showError('Hinweis: ' + response.error + ' - Login sollte trotzdem funktionieren.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.success && response.authenticated) {
|
|
||||||
currentUser = response.user;
|
|
||||||
await loadTools();
|
|
||||||
showLoggedInState();
|
|
||||||
} else {
|
|
||||||
// Not authenticated - show login form
|
|
||||||
showState('login-form');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Init error:', error);
|
|
||||||
showError('Fehler beim Laden: ' + error.message + '. Bitte Extension in chrome://extensions neu laden.');
|
|
||||||
showState('login-form');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show a specific state (loading, login-form, tools-menu)
|
|
||||||
function showState(stateId) {
|
|
||||||
loadingEl.classList.add('hidden');
|
|
||||||
loginFormEl.classList.add('hidden');
|
|
||||||
toolsMenuEl.classList.add('hidden');
|
|
||||||
|
|
||||||
document.getElementById(stateId).classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show logged in state with user info
|
|
||||||
function showLoggedInState() {
|
|
||||||
userEmailEl.textContent = currentUser.email || currentUser.name || 'Benutzer';
|
|
||||||
renderTools();
|
|
||||||
showState('tools-menu');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send message to service worker
|
|
||||||
function sendMessage(message) {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
// Add timeout to prevent hanging
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
console.error('Message timeout:', message.action);
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
error: 'Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.'
|
|
||||||
});
|
|
||||||
}, 2000); // Reduced timeout to 2 seconds for faster feedback
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!chrome.runtime || !chrome.runtime.id) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve({ success: false, error: 'Chrome Runtime nicht verfuegbar' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.sendMessage(message, (response) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
console.error('Chrome runtime error:', chrome.runtime.lastError);
|
|
||||||
resolve({
|
|
||||||
success: false,
|
|
||||||
error: chrome.runtime.lastError.message || 'Service Worker Fehler'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log('Response received:', response);
|
|
||||||
resolve(response || { success: false, error: 'Keine Antwort vom Service Worker' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
console.error('Send message error:', error);
|
|
||||||
resolve({ success: false, error: error.message || 'Fehler beim Senden der Nachricht' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Login form submission
|
|
||||||
authForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const email = emailInput.value.trim();
|
|
||||||
const password = passwordInput.value;
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
showError('Bitte E-Mail und Passwort eingeben');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoginLoading(true);
|
|
||||||
hideError();
|
|
||||||
|
|
||||||
const response = await sendMessage({
|
|
||||||
action: 'LOGIN',
|
|
||||||
email,
|
|
||||||
password
|
|
||||||
});
|
|
||||||
|
|
||||||
setLoginLoading(false);
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
currentUser = response.user;
|
|
||||||
await loadTools();
|
|
||||||
showLoggedInState();
|
|
||||||
|
|
||||||
// Open protected site after successful login
|
|
||||||
openProtectedSite();
|
|
||||||
} else {
|
|
||||||
showError(response.error || 'Anmeldung fehlgeschlagen');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logout button
|
|
||||||
logoutBtn.addEventListener('click', async () => {
|
|
||||||
const response = await sendMessage({ action: 'LOGOUT' });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
currentUser = null;
|
|
||||||
tools = [];
|
|
||||||
emailInput.value = '';
|
|
||||||
passwordInput.value = '';
|
|
||||||
showState('login-form');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open site button
|
|
||||||
openSiteBtn.addEventListener('click', () => {
|
|
||||||
openProtectedSite();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Open protected site in new tab
|
|
||||||
function openProtectedSite() {
|
|
||||||
chrome.tabs.create({ url: PROTECTED_SITE_URL });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load tools settings
|
|
||||||
async function loadTools() {
|
|
||||||
const response = await sendMessage({ action: 'GET_SETTINGS' });
|
|
||||||
if (response.success) {
|
|
||||||
tools = response.tools;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render tools list
|
|
||||||
function renderTools() {
|
|
||||||
toolsListEl.innerHTML = '';
|
|
||||||
|
|
||||||
tools.forEach((tool, index) => {
|
|
||||||
const toolEl = document.createElement('div');
|
|
||||||
toolEl.className = 'tool-item';
|
|
||||||
toolEl.innerHTML = `
|
|
||||||
<div class="tool-header">
|
|
||||||
<span class="tool-name">${escapeHtml(tool.name)}</span>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" data-tool-id="${tool.id}" ${tool.enabled ? 'checked' : ''}>
|
|
||||||
<span class="toggle-slider"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="tool-settings ${tool.enabled ? 'visible' : ''}" id="settings-${tool.id}">
|
|
||||||
${renderToolSettings(tool)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
toolsListEl.appendChild(toolEl);
|
|
||||||
|
|
||||||
// Toggle event listener
|
|
||||||
const toggle = toolEl.querySelector('input[type="checkbox"]');
|
|
||||||
toggle.addEventListener('change', (e) => {
|
|
||||||
handleToolToggle(tool.id, e.target.checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Settings change listeners
|
|
||||||
const settingsInputs = toolEl.querySelectorAll('.setting-row input');
|
|
||||||
settingsInputs.forEach(input => {
|
|
||||||
input.addEventListener('change', (e) => {
|
|
||||||
handleSettingChange(tool.id, e.target.dataset.setting, e.target.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render settings inputs for a tool
|
|
||||||
function renderToolSettings(tool) {
|
|
||||||
if (!tool.settings) return '';
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
for (const [key, value] of Object.entries(tool.settings)) {
|
|
||||||
html += `
|
|
||||||
<div class="setting-row">
|
|
||||||
<label>${escapeHtml(formatSettingName(key))}</label>
|
|
||||||
<input type="text" data-setting="${key}" value="${escapeHtml(value)}">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format setting key to readable name
|
|
||||||
function formatSettingName(key) {
|
|
||||||
return key
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/^./, str => str.toUpperCase())
|
|
||||||
.replace(/_/g, ' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle tool toggle
|
|
||||||
async function handleToolToggle(toolId, enabled) {
|
|
||||||
const tool = tools.find(t => t.id === toolId);
|
|
||||||
if (tool) {
|
|
||||||
tool.enabled = enabled;
|
|
||||||
|
|
||||||
// Show/hide settings
|
|
||||||
const settingsEl = document.getElementById(`settings-${toolId}`);
|
|
||||||
if (settingsEl) {
|
|
||||||
settingsEl.classList.toggle('visible', enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveTools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle setting value change
|
|
||||||
async function handleSettingChange(toolId, settingKey, value) {
|
|
||||||
const tool = tools.find(t => t.id === toolId);
|
|
||||||
if (tool && tool.settings) {
|
|
||||||
tool.settings[settingKey] = value;
|
|
||||||
await saveTools();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save tools to storage
|
|
||||||
async function saveTools() {
|
|
||||||
await sendMessage({ action: 'SAVE_SETTINGS', settings: tools });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show error message
|
|
||||||
function showError(message) {
|
|
||||||
errorMessage.textContent = message;
|
|
||||||
errorMessage.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide error message
|
|
||||||
function hideError() {
|
|
||||||
errorMessage.classList.add('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set login button loading state
|
|
||||||
function setLoginLoading(loading) {
|
|
||||||
loginBtn.disabled = loading;
|
|
||||||
loginBtn.querySelector('.btn-text').classList.toggle('hidden', loading);
|
|
||||||
loginBtn.querySelector('.btn-spinner').classList.toggle('hidden', !loading);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Escape HTML to prevent XSS
|
|
||||||
function escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
108
README.md
108
README.md
@@ -1,28 +1,108 @@
|
|||||||
# Extension & Server Projekt
|
# Extension & Server Projekt
|
||||||
|
|
||||||
Dieses Projekt besteht aus zwei Hauptkomponenten:
|
Dieses Projekt besteht aus drei Hauptkomponenten:
|
||||||
|
|
||||||
## Extension
|
## Extension
|
||||||
- **Zweck**: Frontend und Datenextraktion
|
- **Zweck**: Browser-Extension mit JWT-basierter Authentifizierung
|
||||||
- **Speicherort**: `/Extension`
|
- **Speicherort**: `/Extension`
|
||||||
- Enthält die Browser-Extension für Datenextraktion (z.B. von Amazon)
|
- Empfängt JWT von der Web-App und kommuniziert mit dem Backend
|
||||||
|
|
||||||
## Server
|
## Server (React Web App)
|
||||||
- **Zweck**: Backend-Logik, Verschlüsselung und Sicherheit
|
- **Zweck**: Login-Interface mit Appwrite-Authentifizierung
|
||||||
- **Speicherort**: `/Server`
|
- **Speicherort**: `/Server`
|
||||||
- Basiert auf React mit Appwrite-Integration
|
- Basiert auf React mit Appwrite-Integration
|
||||||
- Implementiert Schutzmechanismen gegen Kopieren der Extension
|
- Sendet JWT an die Extension nach erfolgreichem Login
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
- **Zweck**: Serverseitige JWT-Validierung und API-Endpoints
|
||||||
|
- **Speicherort**: `/Server/backend`
|
||||||
|
- Node/Express Server mit Appwrite-Integration
|
||||||
|
- Validiert JWTs und führt privilegierte Aktionen aus
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Server Setup
|
### 1. Backend starten
|
||||||
|
|
||||||
1. Navigiere zum Server-Ordner: `cd Server`
|
```powershell
|
||||||
2. Dependencies installieren: `npm install`
|
cd Server\backend
|
||||||
3. App starten: `npm run dev`
|
npm install
|
||||||
4. Öffne http://localhost:5173 im Browser
|
npm run dev
|
||||||
5. Klicke auf "Send a ping" um die Appwrite-Verbindung zu testen
|
```
|
||||||
|
|
||||||
### Extension Setup
|
Das Backend läuft auf `http://localhost:3000`
|
||||||
|
|
||||||
Die Extension-Struktur wird in Kürze implementiert.
|
**Wichtig**: Vor dem Start `APPWRITE_API_KEY` in `Server/backend/.env` eintragen!
|
||||||
|
|
||||||
|
### 2. React Web App starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd Server
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Web-App läuft auf `http://localhost:5173` (oder einem anderen Port, den Vite anzeigt)
|
||||||
|
|
||||||
|
**Wichtig**: Stelle sicher, dass `Server/.env` die korrekten Appwrite-Konfigurationswerte enthält:
|
||||||
|
- `VITE_APPWRITE_ENDPOINT`
|
||||||
|
- `VITE_APPWRITE_PROJECT_ID`
|
||||||
|
|
||||||
|
### 3. Extension laden
|
||||||
|
|
||||||
|
1. Öffne Chrome/Edge und gehe zu `chrome://extensions/` (oder `edge://extensions/`)
|
||||||
|
2. Aktiviere **"Entwicklermodus"** (oben rechts)
|
||||||
|
3. Klicke auf **"Entpackte Erweiterung laden"**
|
||||||
|
4. Wähle den `Extension/` Ordner aus
|
||||||
|
5. Die Extension sollte jetzt geladen sein
|
||||||
|
|
||||||
|
### 4. Testen
|
||||||
|
|
||||||
|
1. Öffne die Web-App im Browser (`http://localhost:5173`)
|
||||||
|
2. Du siehst den Login-Sperrbildschirm
|
||||||
|
3. Logge dich mit deinen Appwrite-Credentials ein
|
||||||
|
4. Nach erfolgreichem Login verschwindet der Sperrbildschirm
|
||||||
|
5. Öffne die Extension (Klick auf das Extension-Icon)
|
||||||
|
6. Du solltest "Authed: true" sehen
|
||||||
|
7. Klicke auf "Test action" um die Backend-Verbindung zu testen
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
eship/
|
||||||
|
├── Extension/ # Browser-Extension
|
||||||
|
│ ├── manifest.json
|
||||||
|
│ ├── background.js
|
||||||
|
│ ├── content-script.js
|
||||||
|
│ ├── popup.html
|
||||||
|
│ └── popup.js
|
||||||
|
├── Server/ # React Web App
|
||||||
|
│ ├── src/
|
||||||
|
│ │ └── App.jsx # Login-Sperrbildschirm
|
||||||
|
│ ├── .env # Vite Env-Variablen
|
||||||
|
│ └── backend/ # Node/Express Backend
|
||||||
|
│ ├── server.js
|
||||||
|
│ ├── .env # Backend Env-Variablen
|
||||||
|
│ └── package.json
|
||||||
|
└── setup/ # Setup & Konfiguration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### Backend `.env` (Server/backend/.env)
|
||||||
|
```
|
||||||
|
APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
|
||||||
|
APPWRITE_PROJECT_ID=696b82bb0036d2e547ad
|
||||||
|
APPWRITE_API_KEY=<HIER_DEIN_API_KEY_EINTRAGEN>
|
||||||
|
PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### React App `.env` (Server/.env)
|
||||||
|
```
|
||||||
|
VITE_APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
|
||||||
|
VITE_APPWRITE_PROJECT_ID=696b82bb0036d2e547ad
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extension Backend URL (Extension/background.js)
|
||||||
|
```javascript
|
||||||
|
const BACKEND_URL = "http://localhost:3000"; // Anpassen falls nötig
|
||||||
|
```
|
||||||
|
|||||||
BIN
background.png
Normal file
BIN
background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 778 KiB |
230
db.txt
230
db.txt
@@ -1,230 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Appwrite schema bootstrap (4 tables) for Appwrite Server 1.8.1
|
|
||||||
# Tables: users, accounts, products, product_details
|
|
||||||
#
|
|
||||||
# Notes:
|
|
||||||
# - Appwrite CLI is compatible with Appwrite Server 1.8.x. (see sdk-for-cli README) :contentReference[oaicite:0]{index=0}
|
|
||||||
# - The CLI supports "tables" (create-table, create-*-column, create-index). :contentReference[oaicite:1]{index=1}
|
|
||||||
#
|
|
||||||
# Prereqs:
|
|
||||||
# appwrite login
|
|
||||||
# appwrite init project
|
|
||||||
#
|
|
||||||
# Run:
|
|
||||||
# chmod +x appwrite_schema_v1_8_1.sh
|
|
||||||
# ./appwrite_schema_v1_8_1.sh
|
|
||||||
|
|
||||||
# ---------------- CONFIG ----------------
|
|
||||||
DATABASE_ID="YOUR_DATABASE_ID"
|
|
||||||
|
|
||||||
T_USERS="users"
|
|
||||||
T_ACCOUNTS="accounts"
|
|
||||||
T_PRODUCTS="products"
|
|
||||||
T_PRODUCT_DETAILS="product_details"
|
|
||||||
|
|
||||||
# Permissions: keep minimal for now. Adjust later.
|
|
||||||
# Example roles: any, users, user:<id>, team:<id>
|
|
||||||
PERMS_ANY_CRUD='["create(\\"any\\")","read(\\"any\\")","update(\\"any\\")","delete(\\"any\\")"]'
|
|
||||||
|
|
||||||
# If you want more locked down defaults later, tell me your exact access model.
|
|
||||||
|
|
||||||
# ---------------- HELPERS ----------------
|
|
||||||
try_cmd() {
|
|
||||||
# Run command, do not fail script if it errors (idempotent-ish).
|
|
||||||
# Print what we tried for easy debugging.
|
|
||||||
echo "+ $*"
|
|
||||||
set +e
|
|
||||||
"$@"
|
|
||||||
local rc=$?
|
|
||||||
set -e
|
|
||||||
if [ $rc -ne 0 ]; then
|
|
||||||
echo " (ignored error, rc=$rc)"
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------- CREATE TABLES ----------------
|
|
||||||
# Tip: If any of these fail because option names differ in your CLI build,
|
|
||||||
# run: appwrite databases create-table --help
|
|
||||||
# and replace flags accordingly.
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-table \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_USERS" \
|
|
||||||
--name "users" \
|
|
||||||
--permissions "$PERMS_ANY_CRUD" \
|
|
||||||
--row-security false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-table \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_ACCOUNTS" \
|
|
||||||
--name "accounts" \
|
|
||||||
--permissions "$PERMS_ANY_CRUD" \
|
|
||||||
--row-security false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-table \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCTS" \
|
|
||||||
--name "products" \
|
|
||||||
--permissions "$PERMS_ANY_CRUD" \
|
|
||||||
--row-security false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-table \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCT_DETAILS" \
|
|
||||||
--name "product_details" \
|
|
||||||
--permissions "$PERMS_ANY_CRUD" \
|
|
||||||
--row-security false
|
|
||||||
|
|
||||||
# ---------------- USERS COLUMNS ----------------
|
|
||||||
# You originally wanted basically no fields here. Some people keep this table empty
|
|
||||||
# (using only system fields), but depending on tooling, an empty table can be annoying.
|
|
||||||
# Keep one optional column for future user settings/notes.
|
|
||||||
try_cmd appwrite databases create-string-column \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_USERS" \
|
|
||||||
--key "user_note" \
|
|
||||||
--size 255 \
|
|
||||||
--required false \
|
|
||||||
--array false
|
|
||||||
|
|
||||||
# ---------------- ACCOUNTS COLUMNS ----------------
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_owner_user_id" --size 64 --required false --array false
|
|
||||||
try_cmd appwrite databases create-boolean-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_managed" --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_platform" --elements '["amazon","ebay"]' --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_platform_account_id" --size 255 --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_platform_market" --size 32 --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_shop_name" --size 255 --required false --array false
|
|
||||||
try_cmd appwrite databases create-url-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_url" --required false --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_ACCOUNTS" --key "account_status" --elements '["active","unknown","disabled"]' --required false --array false
|
|
||||||
|
|
||||||
# ---------------- ACCOUNTS INDEXES ----------------
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_ACCOUNTS" \
|
|
||||||
--key "accounts_unique_platform_market_accountid" \
|
|
||||||
--type "unique" \
|
|
||||||
--columns '["account_platform","account_platform_market","account_platform_account_id"]'
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_ACCOUNTS" \
|
|
||||||
--key "accounts_by_owner_user" \
|
|
||||||
--type "key" \
|
|
||||||
--columns '["account_owner_user_id"]'
|
|
||||||
|
|
||||||
# ---------------- PRODUCTS COLUMNS ----------------
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_account_id" --size 64 --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_platform" --elements '["amazon","ebay"]' --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_platform_market" --size 32 --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_platform_product_id" --size 255 --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_title" --size 1024 --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_category" --size 255 --required false --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-float-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_price" --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_currency" --size 8 --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-integer-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_quantity" --required false --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_condition" --elements '["new","used_like_new","used_good","used_ok","parts"]' --required false --array false
|
|
||||||
try_cmd appwrite databases create-url-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_url" --required false --array false
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCTS" --key "product_status" --elements '["active","ended","unknown"]' --required false --array false
|
|
||||||
|
|
||||||
# ---------------- PRODUCTS INDEXES ----------------
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCTS" \
|
|
||||||
--key "products_by_account" \
|
|
||||||
--type "key" \
|
|
||||||
--columns '["product_account_id"]'
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCTS" \
|
|
||||||
--key "products_unique_account_platformproductid" \
|
|
||||||
--type "unique" \
|
|
||||||
--columns '["product_account_id","product_platform_product_id"]'
|
|
||||||
|
|
||||||
# ---------------- PRODUCT_DETAILS COLUMNS ----------------
|
|
||||||
# 1:1 to products
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_product_id" --size 64 --required true --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_platform" --elements '["amazon","ebay"]' --required true --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_platform_market" --size 32 --required false --array false
|
|
||||||
|
|
||||||
# Identifiers
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_gtin" --size 32 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_ean" --size 32 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_upc" --size 32 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_isbn" --size 32 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_mpn" --size 64 --required false --array false
|
|
||||||
|
|
||||||
# Platform IDs
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_amazon_asin" --size 32 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_ebay_epid" --size 64 --required false --array false
|
|
||||||
|
|
||||||
# Brand / model
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_brand" --size 255 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_manufacturer" --size 255 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_model_name" --size 255 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_model_number" --size 255 --required false --array false
|
|
||||||
|
|
||||||
# Content
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_short_description" --size 2048 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_long_description" --size 8192 --required false --array false
|
|
||||||
|
|
||||||
# Bullet points (no arrays, no JSON)
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_1" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_2" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_3" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_4" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_5" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_6" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_7" --size 512 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_bullet_8" --size 512 --required false --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_search_terms" --size 1024 --required false --array false
|
|
||||||
|
|
||||||
# Variants / item specifics (common)
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_color" --size 128 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_size" --size 128 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_material" --size 128 --required false --array false
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_pattern" --size 128 --required false --array false
|
|
||||||
|
|
||||||
# Shipping measurements
|
|
||||||
try_cmd appwrite databases create-float-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_length" --required false --array false
|
|
||||||
try_cmd appwrite databases create-float-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_width" --required false --array false
|
|
||||||
try_cmd appwrite databases create-float-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_height" --required false --array false
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_dimension_unit" --elements '["mm","cm","m","in"]' --required false --array false
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-float-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_weight" --required false --array false
|
|
||||||
try_cmd appwrite databases create-enum-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_weight_unit" --elements '["g","kg","oz","lb"]' --required false --array false
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
try_cmd appwrite databases create-string-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_country_of_origin" --size 64 --required false --array false
|
|
||||||
try_cmd appwrite databases create-integer-column --database-id "$DATABASE_ID" --table-id "$T_PRODUCT_DETAILS" --key "product_detail_package_quantity" --required false --array false
|
|
||||||
|
|
||||||
# ---------------- PRODUCT_DETAILS INDEXES ----------------
|
|
||||||
# One details row per product
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCT_DETAILS" \
|
|
||||||
--key "product_details_unique_product_id" \
|
|
||||||
--type "unique" \
|
|
||||||
--columns '["product_detail_product_id"]'
|
|
||||||
|
|
||||||
try_cmd appwrite databases create-index \
|
|
||||||
--database-id "$DATABASE_ID" \
|
|
||||||
--table-id "$T_PRODUCT_DETAILS" \
|
|
||||||
--key "product_details_by_platform" \
|
|
||||||
--type "key" \
|
|
||||||
--columns '["product_detail_platform"]'
|
|
||||||
|
|
||||||
echo "Done. 4 tables ensured: users, accounts, products, product_details"
|
|
||||||
BIN
logo128.png
Normal file
BIN
logo128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
BIN
logo16.png
Normal file
BIN
logo16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
logo48.png
Normal file
BIN
logo48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,53 +0,0 @@
|
|||||||
# Appwrite API Key Setup
|
|
||||||
|
|
||||||
## API Key erstellen
|
|
||||||
|
|
||||||
1. **Appwrite Console oeffnen**: https://cloud.appwrite.io
|
|
||||||
2. **Projekt auswaehlen**: Waehle dein Projekt (ID: `696b82bb0036d2e547ad`)
|
|
||||||
3. **Settings > API Keys** navigieren
|
|
||||||
4. **"Create API Key"** klicken
|
|
||||||
5. **Konfiguration**:
|
|
||||||
- **Name**: `EShip Extension Key` (oder beliebiger Name)
|
|
||||||
- **Scopes**:
|
|
||||||
- `users.read` - Benutzer lesen
|
|
||||||
- `users.write` - Benutzer erstellen/bearbeiten
|
|
||||||
- `sessions.write` - Sessions erstellen
|
|
||||||
- Optional: Weitere Scopes je nach Bedarf
|
|
||||||
- **Expiration**: Optional (leer lassen fuer unbegrenzt)
|
|
||||||
6. **"Create"** klicken
|
|
||||||
7. **API Key kopieren** - WICHTIG: Der Key wird nur einmal angezeigt!
|
|
||||||
|
|
||||||
## API Key in Extension konfigurieren
|
|
||||||
|
|
||||||
1. Oeffne `Extension/config.js`
|
|
||||||
2. Fuege den API Key hinzu:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
var APPWRITE_CONFIG = {
|
|
||||||
endpoint: 'https://cloud.appwrite.io/v1',
|
|
||||||
projectId: '696b82bb0036d2e547ad',
|
|
||||||
apiKey: 'DEIN_API_KEY_HIER' // Hier den kopierten Key einfuegen
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Extension neu laden in `chrome://extensions`
|
|
||||||
|
|
||||||
## Sicherheit
|
|
||||||
|
|
||||||
- **NIEMALS** den API Key in Git committen
|
|
||||||
- Der API Key sollte nur in der Extension verwendet werden
|
|
||||||
- Bei Verlust: Alten Key loeschen und neuen erstellen
|
|
||||||
- Verwende unterschiedliche Keys fuer Development und Production
|
|
||||||
|
|
||||||
## Alternative: Environment-basierte Konfiguration
|
|
||||||
|
|
||||||
Fuer Production kannst du den API Key auch ueber Chrome Storage setzen:
|
|
||||||
|
|
||||||
1. In der Extension: `chrome.storage.local.set({ apiKey: 'DEIN_KEY' })`
|
|
||||||
2. Im Service Worker: Key aus Storage laden
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
- **"Invalid API Key"**: Pruefe, ob der Key korrekt kopiert wurde (keine Leerzeichen)
|
|
||||||
- **"Insufficient permissions"**: Pruefe die Scopes des API Keys
|
|
||||||
- **"Key expired"**: Erstelle einen neuen API Key
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# API Server Setup
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
Chrome Extensions haben unterschiedliche IDs bei jeder Installation. Appwrite erfordert Platform-Registrierung, was nicht praktikabel ist.
|
|
||||||
|
|
||||||
## Lösung
|
|
||||||
|
|
||||||
Ein Express API-Server fungiert als Proxy zwischen Extension und Appwrite. Der Server verwendet den API Key (server-seitig, keine Platform-Registrierung nötig) und erstellt Sessions fuer die Extension.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. Dependencies installieren:
|
|
||||||
```bash
|
|
||||||
cd Server
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. API Server starten:
|
|
||||||
```bash
|
|
||||||
npm run dev:api
|
|
||||||
```
|
|
||||||
|
|
||||||
Der Server laeuft auf `http://localhost:3001`
|
|
||||||
|
|
||||||
## Beide Server gleichzeitig starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev:all
|
|
||||||
```
|
|
||||||
|
|
||||||
Startet sowohl Vite (Port 5173) als auch API Server (Port 3001).
|
|
||||||
|
|
||||||
## Umgebungsvariablen (optional)
|
|
||||||
|
|
||||||
Erstelle eine `.env` Datei im `Server/` Ordner:
|
|
||||||
|
|
||||||
```
|
|
||||||
APPWRITE_ENDPOINT=https://appwrite.webklar.com/v1
|
|
||||||
APPWRITE_PROJECT_ID=696b82bb0036d2e547ad
|
|
||||||
APPWRITE_API_KEY=dein_api_key_hier
|
|
||||||
```
|
|
||||||
|
|
||||||
Falls nicht gesetzt, werden die Default-Werte aus `api-server.js` verwendet.
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
- `POST /api/extension/login` - Login mit Email/Password
|
|
||||||
- `GET /api/extension/auth` - Prueft Auth-Status
|
|
||||||
- `POST /api/extension/logout` - Logout
|
|
||||||
- `GET /api/health` - Health Check
|
|
||||||
|
|
||||||
## Vorteile
|
|
||||||
|
|
||||||
- Keine Platform-Registrierung in Appwrite noetig
|
|
||||||
- Funktioniert fuer alle Extension-Installationen
|
|
||||||
- API Key bleibt sicher auf dem Server
|
|
||||||
- Einfache Skalierung
|
|
||||||
174
setup/USERS_COLLECTION_SETUP.md
Normal file
174
setup/USERS_COLLECTION_SETUP.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# Users Collection Setup & Permissions
|
||||||
|
|
||||||
|
Diese Anleitung erklärt, wie du die `users` Collection in Appwrite einrichtest und die richtigen Permissions setzt.
|
||||||
|
|
||||||
|
## Problem: 401 Unauthorized
|
||||||
|
|
||||||
|
Wenn du beim Erstellen eines User-Dokuments einen **401 Unauthorized** Fehler bekommst, bedeutet das, dass die Permissions der Collection nicht richtig gesetzt sind.
|
||||||
|
|
||||||
|
## Lösung: Collection erstellen und Permissions setzen
|
||||||
|
|
||||||
|
### Schritt 1: Collection-ID ermitteln oder erstellen
|
||||||
|
|
||||||
|
#### Option A: Collection bereits vorhanden
|
||||||
|
|
||||||
|
Prüfe, ob die Collection bereits existiert:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Server
|
||||||
|
appwrite login
|
||||||
|
appwrite databases list-collections --database-id eship-db
|
||||||
|
```
|
||||||
|
|
||||||
|
Suche nach einer Collection mit dem Namen "users" oder einer ID, die du verwenden möchtest.
|
||||||
|
|
||||||
|
#### Option B: Collection erstellen
|
||||||
|
|
||||||
|
Falls die Collection nicht existiert, erstelle sie:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
appwrite databases create-collection \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--name "Users"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: Notiere dir die `$id` der erstellten Collection. Falls die Collection-ID nicht "users" ist, musst du sie in der `.env` Datei setzen:
|
||||||
|
|
||||||
|
```
|
||||||
|
VITE_APPWRITE_USERS_COLLECTION_ID=<deine-collection-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schritt 2: Attribute hinzufügen
|
||||||
|
|
||||||
|
Die Collection braucht ein `user_name` Feld (String):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
appwrite databases create-string-attribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--key user_name \
|
||||||
|
--size 255 \
|
||||||
|
--required true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wichtig**: Warte, bis das Attribute erstellt wurde (Status: "available"). Das kann einige Sekunden dauern.
|
||||||
|
|
||||||
|
### Schritt 3: Permissions setzen
|
||||||
|
|
||||||
|
Das ist der wichtigste Schritt! Die Collection muss erlauben, dass:
|
||||||
|
- Authentifizierte User Dokumente lesen können
|
||||||
|
- Authentifizierte User Dokumente erstellen können (für ihr eigenes Dokument)
|
||||||
|
|
||||||
|
#### Über die Appwrite-Konsole (empfohlen)
|
||||||
|
|
||||||
|
1. Öffne die Appwrite-Konsole: `https://appwrite.webklar.com`
|
||||||
|
2. Gehe zu: **Databases** → **eship-db** → **users** Collection
|
||||||
|
3. Klicke auf **Settings** → **Permissions**
|
||||||
|
4. Füge folgende Permissions hinzu:
|
||||||
|
|
||||||
|
**Create Permission:**
|
||||||
|
- Role: `users`
|
||||||
|
- Erlaube: **create**
|
||||||
|
|
||||||
|
**Read Permission:**
|
||||||
|
- Role: `users`
|
||||||
|
- Erlaube: **read**
|
||||||
|
|
||||||
|
**Update Permission (optional, falls du Updates brauchst):**
|
||||||
|
- Role: `users`
|
||||||
|
- Erlaube: **update**
|
||||||
|
|
||||||
|
**Delete Permission (optional):**
|
||||||
|
- Role: `users`
|
||||||
|
- Erlaube: **delete**
|
||||||
|
|
||||||
|
#### Über die CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create Permission
|
||||||
|
appwrite databases create-collection-create-rule \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--role users
|
||||||
|
|
||||||
|
# Read Permission
|
||||||
|
appwrite databases create-collection-read-rule \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--role users
|
||||||
|
|
||||||
|
# Optional: Update Permission
|
||||||
|
appwrite databases create-collection-update-rule \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--role users
|
||||||
|
|
||||||
|
# Optional: Delete Permission
|
||||||
|
appwrite databases create-collection-delete-rule \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users \
|
||||||
|
--role users
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hinweis**: Die CLI-Befehle können je nach Appwrite-Version variieren. Wenn sie nicht funktionieren, verwende die Web-Konsole.
|
||||||
|
|
||||||
|
### Schritt 4: Document-ID-Permissions
|
||||||
|
|
||||||
|
Da wir die Auth-User-ID als Document-ID verwenden, muss Appwrite erlauben, dass User ihre eigene Document-ID verwenden können.
|
||||||
|
|
||||||
|
In der Appwrite-Konsole:
|
||||||
|
1. Gehe zu: **Databases** → **eship-db** → **users** → **Settings**
|
||||||
|
2. Stelle sicher, dass **"Allow users to specify their own document IDs"** aktiviert ist
|
||||||
|
- Oder setze: **"Document ID Generation"** auf **"User provided"**
|
||||||
|
|
||||||
|
### Schritt 5: Überprüfung
|
||||||
|
|
||||||
|
Nach dem Setup sollte:
|
||||||
|
- ✅ Ein eingeloggter User Dokumente lesen können
|
||||||
|
- ✅ Ein eingeloggter User sein eigenes Dokument erstellen können (mit seiner Auth-User-ID als Document-ID)
|
||||||
|
|
||||||
|
## Häufige Probleme
|
||||||
|
|
||||||
|
### Problem: "Collection not found"
|
||||||
|
|
||||||
|
**Lösung**: Prüfe, ob die Collection-ID korrekt ist:
|
||||||
|
```bash
|
||||||
|
appwrite databases list-collections --database-id eship-db
|
||||||
|
```
|
||||||
|
Stelle sicher, dass `VITE_APPWRITE_USERS_COLLECTION_ID` in der `.env` die richtige ID enthält.
|
||||||
|
|
||||||
|
### Problem: "Attribute not found"
|
||||||
|
|
||||||
|
**Lösung**: Prüfe, ob das `user_name` Attribute existiert:
|
||||||
|
```bash
|
||||||
|
appwrite databases list-attributes \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem: "Permission denied"
|
||||||
|
|
||||||
|
**Lösung**: Stelle sicher, dass die Permissions für die Rolle `users` gesetzt sind. In der Appwrite-Konsole:
|
||||||
|
- Gehe zu **Settings** → **Permissions**
|
||||||
|
- Prüfe, ob `create` und `read` für `users` aktiviert sind
|
||||||
|
|
||||||
|
## Alternative: Restriktivere Permissions (sicherer)
|
||||||
|
|
||||||
|
Falls du sicherer sein möchtest, dass User nur ihr eigenes Dokument lesen/erstellen können:
|
||||||
|
|
||||||
|
1. In der Appwrite-Konsole: Gehe zu **Settings** → **Permissions**
|
||||||
|
2. Verwende **Custom Permissions** mit Filtern:
|
||||||
|
- Read: `$userId = request.auth.userId`
|
||||||
|
- Create: `$userId = request.auth.userId`
|
||||||
|
- Update: `$userId = request.auth.userId`
|
||||||
|
|
||||||
|
Dies stellt sicher, dass User nur Zugriff auf ihr eigenes Dokument haben.
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
Nach dem Setup:
|
||||||
|
1. Lade die Web-App neu
|
||||||
|
2. Logge dich ein
|
||||||
|
3. Du solltest den Welcome-Screen sehen
|
||||||
|
4. Beim Klick auf "Jetzt starten" sollte das User-Dokument erstellt werden (kein 401-Fehler mehr)
|
||||||
Reference in New Issue
Block a user