diff --git a/.cursor/debug.log b/.cursor/debug.log new file mode 100644 index 0000000..baef34e --- /dev/null +++ b/.cursor/debug.log @@ -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"} diff --git a/Extension/README.md b/Extension/README.md index bba79c5..2dc0985 100644 --- a/Extension/README.md +++ b/Extension/README.md @@ -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) -- **Website-Schutz**: Blockiert die Web-App fuer nicht-authentifizierte Benutzer -- **Tools-System**: Aktivierbare Tools mit individuellen Einstellungen -- **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) +- Frontend-Benutzeroberfläche +- Datenextraktion von Websites (z.B. Amazon) +- Kommunikation mit dem Server für Verschlüsselung und Authentifizierung diff --git a/Extension/background.js b/Extension/background.js new file mode 100644 index 0000000..2cf43f0 --- /dev/null +++ b/Extension/background.js @@ -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 + +// 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(); +} diff --git a/Extension/background/service-worker.js b/Extension/background/service-worker.js deleted file mode 100644 index feea162..0000000 --- a/Extension/background/service-worker.js +++ /dev/null @@ -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'); diff --git a/Extension/config.js b/Extension/config.js deleted file mode 100644 index f7176af..0000000 --- a/Extension/config.js +++ /dev/null @@ -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 }; -} diff --git a/Extension/content-script.js b/Extension/content-script.js new file mode 100644 index 0000000..d839291 --- /dev/null +++ b/Extension/content-script.js @@ -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; + } +}); diff --git a/Extension/content/content-script.js b/Extension/content/content-script.js deleted file mode 100644 index d09a197..0000000 --- a/Extension/content/content-script.js +++ /dev/null @@ -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 = ` - -
🔒
-

Zugriff gesperrt

-

Diese Website ist geschuetzt und erfordert eine Authentifizierung ueber die EShip Browser-Extension.

-
-

So melden Sie sich an:

-
    -
  1. Klicken Sie auf das EShip-Icon in der Browser-Toolbar
  2. -
  3. Geben Sie Ihre E-Mail und Passwort ein
  4. -
  5. Klicken Sie auf "Anmelden"
  6. -
  7. Die Seite wird automatisch freigeschaltet
  8. -
-
- `; - - 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 }); - } - -})(); diff --git a/Extension/ebay-content-script.js b/Extension/ebay-content-script.js new file mode 100644 index 0000000..344be80 --- /dev/null +++ b/Extension/ebay-content-script.js @@ -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: {} + }; + } +} \ No newline at end of file diff --git a/Extension/lib/appwrite.min.js b/Extension/lib/appwrite.min.js deleted file mode 100644 index b218f77..0000000 --- a/Extension/lib/appwrite.min.js +++ /dev/null @@ -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); diff --git a/Extension/manifest.json b/Extension/manifest.json index 731747a..c23e53a 100644 --- a/Extension/manifest.json +++ b/Extension/manifest.json @@ -1,44 +1,49 @@ { "manifest_version": 3, - "name": "EShip Auth Extension", - "version": "1.0.0", - "description": "Authentication extension for EShip protected web app", - - "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" - }, - + "name": "Protected Extension", + "version": "0.1.0", + "action": { "default_popup": "popup.html" }, + "background": { "service_worker": "background.js", "type": "module" }, "content_scripts": [ { - "matches": ["http://localhost:5173/*", "http://localhost:3000/*"], - "js": ["content/content-script.js"], - "run_at": "document_start" + "matches": ["http://localhost:*/*", "https://*/*"], + "js": ["content-script.js"], + "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" } ], - - - "web_accessible_resources": [ - { - "resources": ["lib/*", "config.js"], - "matches": ["http://localhost:5173/*", "http://localhost:3000/*"] - } - ] + "permissions": ["tabs", "scripting", "storage"], + "host_permissions": [ + "https://*/*", + "*://*.ebay.de/*", + "*://*.ebay.com/*", + "*://*.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://*/*"] + } } diff --git a/Extension/popup.html b/Extension/popup.html new file mode 100644 index 0000000..4e71890 --- /dev/null +++ b/Extension/popup.html @@ -0,0 +1,8 @@ + + + + +
+ + + diff --git a/Extension/popup.js b/Extension/popup.js new file mode 100644 index 0000000..5f54b2d --- /dev/null +++ b/Extension/popup.js @@ -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 = ` +
+
Extension
+
Authed: ${jwt ? "true" : "false"}
+ +
+
+ `; + + 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(); diff --git a/Extension/popup/popup.css b/Extension/popup/popup.css deleted file mode 100644 index abdfc5a..0000000 --- a/Extension/popup/popup.css +++ /dev/null @@ -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; -} diff --git a/Extension/popup/popup.html b/Extension/popup/popup.html deleted file mode 100644 index 99a6d96..0000000 --- a/Extension/popup/popup.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - EShip Extension - - - -
-
-

EShip

-
- - -
-
-

Laden...

-
- - - - - - -
- - - - diff --git a/Extension/popup/popup.js b/Extension/popup/popup.js deleted file mode 100644 index aa24f4b..0000000 --- a/Extension/popup/popup.js +++ /dev/null @@ -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 = ` -
- ${escapeHtml(tool.name)} - -
-
- ${renderToolSettings(tool)} -
- `; - 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 += ` -
- - -
- `; - } - 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; -} diff --git a/README.md b/README.md index d662017..d4afed3 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,108 @@ # Extension & Server Projekt -Dieses Projekt besteht aus zwei Hauptkomponenten: +Dieses Projekt besteht aus drei Hauptkomponenten: ## Extension -- **Zweck**: Frontend und Datenextraktion +- **Zweck**: Browser-Extension mit JWT-basierter Authentifizierung - **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 -- **Zweck**: Backend-Logik, Verschlüsselung und Sicherheit +## Server (React Web App) +- **Zweck**: Login-Interface mit Appwrite-Authentifizierung - **Speicherort**: `/Server` - 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 -### Server Setup +### 1. Backend starten -1. Navigiere zum Server-Ordner: `cd Server` -2. Dependencies installieren: `npm install` -3. App starten: `npm run dev` -4. Öffne http://localhost:5173 im Browser -5. Klicke auf "Send a ping" um die Appwrite-Verbindung zu testen +```powershell +cd Server\backend +npm install +npm run dev +``` -### 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= +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 +``` diff --git a/background.png b/background.png new file mode 100644 index 0000000..8f91c65 Binary files /dev/null and b/background.png differ diff --git a/db.txt b/db.txt deleted file mode 100644 index 7eed06b..0000000 --- a/db.txt +++ /dev/null @@ -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:, team: -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" diff --git a/logo128.png b/logo128.png new file mode 100644 index 0000000..0d6261d Binary files /dev/null and b/logo128.png differ diff --git a/logo16.png b/logo16.png new file mode 100644 index 0000000..e63ad7d Binary files /dev/null and b/logo16.png differ diff --git a/logo48.png b/logo48.png new file mode 100644 index 0000000..00e00af Binary files /dev/null and b/logo48.png differ diff --git a/setup/API_KEY_SETUP.md b/setup/API_KEY_SETUP.md deleted file mode 100644 index 2596613..0000000 --- a/setup/API_KEY_SETUP.md +++ /dev/null @@ -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 diff --git a/setup/API_SERVER_SETUP.md b/setup/API_SERVER_SETUP.md deleted file mode 100644 index 5819840..0000000 --- a/setup/API_SERVER_SETUP.md +++ /dev/null @@ -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 diff --git a/setup/USERS_COLLECTION_SETUP.md b/setup/USERS_COLLECTION_SETUP.md new file mode 100644 index 0000000..0f851ab --- /dev/null +++ b/setup/USERS_COLLECTION_SETUP.md @@ -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= +``` + +### 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)