Sure! Pl
This commit is contained in:
189
Extension/README.md
Normal file
189
Extension/README.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# EShip Browser Extension
|
||||
|
||||
Chrome Extension (Manifest V3) fuer die Authentifizierung und Tool-Verwaltung der EShip Web-App.
|
||||
|
||||
## 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)
|
||||
419
Extension/background/service-worker.js
Normal file
419
Extension/background/service-worker.js
Normal file
@@ -0,0 +1,419 @@
|
||||
// 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');
|
||||
17
Extension/config.js
Normal file
17
Extension/config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 };
|
||||
}
|
||||
310
Extension/content/content-script.js
Normal file
310
Extension/content/content-script.js
Normal file
@@ -0,0 +1,310 @@
|
||||
// EShip Extension Content Script
|
||||
// Injects into protected website to enforce auth and apply tools
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// State
|
||||
let isAuthenticated = false;
|
||||
let currentTools = [];
|
||||
let blockedOverlay = null;
|
||||
|
||||
// Initialize on page load
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
// Check auth status immediately
|
||||
const authResponse = await sendMessage({ action: 'CHECK_AUTH' });
|
||||
|
||||
if (authResponse.success && authResponse.authenticated) {
|
||||
isAuthenticated = true;
|
||||
await loadAndApplyTools();
|
||||
} else {
|
||||
isAuthenticated = false;
|
||||
showBlockedScreen();
|
||||
}
|
||||
|
||||
// Listen for messages from service worker (settings updates)
|
||||
chrome.runtime.onMessage.addListener(handleMessage);
|
||||
}
|
||||
|
||||
// Send message to service worker
|
||||
function sendMessage(message) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(message, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve({ success: false, error: chrome.runtime.lastError.message });
|
||||
} else {
|
||||
resolve(response || { success: false, error: 'No response' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle incoming messages
|
||||
function handleMessage(request, sender, sendResponse) {
|
||||
switch (request.action) {
|
||||
case 'SETTINGS_UPDATED':
|
||||
currentTools = request.tools;
|
||||
applyTools();
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
|
||||
case 'AUTH_CHANGED':
|
||||
if (request.authenticated) {
|
||||
isAuthenticated = true;
|
||||
hideBlockedScreen();
|
||||
loadAndApplyTools();
|
||||
} else {
|
||||
isAuthenticated = false;
|
||||
showBlockedScreen();
|
||||
}
|
||||
sendResponse({ success: true });
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Show blocked screen when not authenticated
|
||||
function showBlockedScreen() {
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', createBlockedOverlay);
|
||||
} else {
|
||||
createBlockedOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
function createBlockedOverlay() {
|
||||
// Remove existing overlay if any
|
||||
hideBlockedScreen();
|
||||
|
||||
// Create overlay
|
||||
blockedOverlay = document.createElement('div');
|
||||
blockedOverlay.id = 'eship-blocked-overlay';
|
||||
blockedOverlay.innerHTML = `
|
||||
<style>
|
||||
#eship-blocked-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999999;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
#eship-blocked-overlay .blocked-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
#eship-blocked-overlay h1 {
|
||||
color: #FD366E;
|
||||
font-size: 28px;
|
||||
margin: 0 0 16px 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
#eship-blocked-overlay p {
|
||||
color: #9ca3af;
|
||||
font-size: 16px;
|
||||
margin: 0 0 32px 0;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
#eship-blocked-overlay .instructions {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
max-width: 400px;
|
||||
}
|
||||
#eship-blocked-overlay .instructions h2 {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
margin: 0 0 16px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
#eship-blocked-overlay .instructions ol {
|
||||
color: #d1d5db;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
line-height: 2;
|
||||
}
|
||||
#eship-blocked-overlay .instructions li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
</style>
|
||||
<div class="blocked-icon">🔒</div>
|
||||
<h1>Zugriff gesperrt</h1>
|
||||
<p>Diese Website ist geschuetzt und erfordert eine Authentifizierung ueber die EShip Browser-Extension.</p>
|
||||
<div class="instructions">
|
||||
<h2>So melden Sie sich an:</h2>
|
||||
<ol>
|
||||
<li>Klicken Sie auf das EShip-Icon in der Browser-Toolbar</li>
|
||||
<li>Geben Sie Ihre E-Mail und Passwort ein</li>
|
||||
<li>Klicken Sie auf "Anmelden"</li>
|
||||
<li>Die Seite wird automatisch freigeschaltet</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(blockedOverlay);
|
||||
|
||||
// Prevent scrolling on body
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Hide blocked screen
|
||||
function hideBlockedScreen() {
|
||||
if (blockedOverlay) {
|
||||
blockedOverlay.remove();
|
||||
blockedOverlay = null;
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Load and apply tool settings
|
||||
async function loadAndApplyTools() {
|
||||
const response = await sendMessage({ action: 'GET_SETTINGS' });
|
||||
if (response.success) {
|
||||
currentTools = response.tools;
|
||||
applyTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Apply all enabled tools
|
||||
function applyTools() {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
// Wait for DOM to be ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => executeTools());
|
||||
} else {
|
||||
executeTools();
|
||||
}
|
||||
}
|
||||
|
||||
function executeTools() {
|
||||
currentTools.forEach(tool => {
|
||||
if (tool.enabled) {
|
||||
applyTool(tool);
|
||||
} else {
|
||||
removeTool(tool);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Apply a specific tool
|
||||
function applyTool(tool) {
|
||||
switch (tool.id) {
|
||||
case 'highlight_prices':
|
||||
applyHighlightPrices(tool.settings);
|
||||
break;
|
||||
// Add more tools here as needed
|
||||
default:
|
||||
console.log('[EShip] Unknown tool:', tool.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a specific tool's effects
|
||||
function removeTool(tool) {
|
||||
switch (tool.id) {
|
||||
case 'highlight_prices':
|
||||
removeHighlightPrices(tool.settings);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL: Highlight Prices
|
||||
// ============================================
|
||||
|
||||
function applyHighlightPrices(settings) {
|
||||
const selector = settings.selector || '.price';
|
||||
const borderColor = settings.borderColor || '#ff0000';
|
||||
const borderWidth = settings.borderWidth || '2px';
|
||||
|
||||
// Add custom style if not exists
|
||||
let styleEl = document.getElementById('eship-highlight-prices-style');
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
styleEl.id = 'eship-highlight-prices-style';
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
|
||||
styleEl.textContent = `
|
||||
${selector} {
|
||||
border: ${borderWidth} solid ${borderColor} !important;
|
||||
box-shadow: 0 0 8px ${borderColor}40 !important;
|
||||
border-radius: 4px !important;
|
||||
padding: 2px 4px !important;
|
||||
transition: all 0.2s ease !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// Also add data attribute to track highlighted elements
|
||||
document.querySelectorAll(selector).forEach(el => {
|
||||
el.dataset.eshipHighlighted = 'true';
|
||||
});
|
||||
|
||||
console.log('[EShip] Highlight Prices applied with selector:', selector);
|
||||
}
|
||||
|
||||
function removeHighlightPrices(settings) {
|
||||
// Remove style element
|
||||
const styleEl = document.getElementById('eship-highlight-prices-style');
|
||||
if (styleEl) {
|
||||
styleEl.remove();
|
||||
}
|
||||
|
||||
// Remove data attributes
|
||||
document.querySelectorAll('[data-eship-highlighted]').forEach(el => {
|
||||
delete el.dataset.eshipHighlighted;
|
||||
});
|
||||
|
||||
console.log('[EShip] Highlight Prices removed');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Mutation Observer for dynamic content
|
||||
// ============================================
|
||||
|
||||
// Re-apply tools when new content is added
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (!isAuthenticated) return;
|
||||
|
||||
let shouldReapply = false;
|
||||
mutations.forEach(mutation => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
shouldReapply = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldReapply) {
|
||||
// Debounce reapplication
|
||||
clearTimeout(observer.timeout);
|
||||
observer.timeout = setTimeout(() => {
|
||||
applyTools();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Start observing once DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
});
|
||||
} else {
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
})();
|
||||
209
Extension/lib/appwrite.min.js
vendored
Normal file
209
Extension/lib/appwrite.min.js
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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);
|
||||
44
Extension/manifest.json
Normal file
44
Extension/manifest.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"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"
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://localhost:5173/*", "http://localhost:3000/*"],
|
||||
"js": ["content/content-script.js"],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["lib/*", "config.js"],
|
||||
"matches": ["http://localhost:5173/*", "http://localhost:3000/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
315
Extension/popup/popup.css
Normal file
315
Extension/popup/popup.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/* 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;
|
||||
}
|
||||
62
Extension/popup/popup.html
Normal file
62
Extension/popup/popup.html
Normal file
@@ -0,0 +1,62 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=400, initial-scale=1.0">
|
||||
<title>EShip Extension</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>EShip</h1>
|
||||
</header>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading" class="state">
|
||||
<div class="spinner"></div>
|
||||
<p>Laden...</p>
|
||||
</div>
|
||||
|
||||
<!-- Login Form (logged out state) -->
|
||||
<div id="login-form" class="state hidden">
|
||||
<h2>Anmelden</h2>
|
||||
<form id="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||
</div>
|
||||
<div id="error-message" class="error hidden"></div>
|
||||
<button type="submit" id="login-btn" class="btn btn-primary">
|
||||
<span class="btn-text">Anmelden</span>
|
||||
<span class="btn-spinner hidden"></span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Tools Menu (logged in state) -->
|
||||
<div id="tools-menu" class="state hidden">
|
||||
<div class="user-info">
|
||||
<span class="user-icon">👤</span>
|
||||
<span id="user-email"></span>
|
||||
<button id="logout-btn" class="btn btn-small btn-secondary">Abmelden</button>
|
||||
</div>
|
||||
|
||||
<h2>Tools</h2>
|
||||
<div id="tools-list">
|
||||
<!-- Tools will be rendered here dynamically -->
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="open-site-btn" class="btn btn-primary">Website oeffnen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="popup.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
319
Extension/popup/popup.js
Normal file
319
Extension/popup/popup.js
Normal file
@@ -0,0 +1,319 @@
|
||||
// EShip Extension Popup Logic
|
||||
|
||||
// DOM Elements
|
||||
const loadingEl = document.getElementById('loading');
|
||||
const loginFormEl = document.getElementById('login-form');
|
||||
const toolsMenuEl = document.getElementById('tools-menu');
|
||||
const authForm = document.getElementById('auth-form');
|
||||
const emailInput = document.getElementById('email');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const userEmailEl = document.getElementById('user-email');
|
||||
const toolsListEl = document.getElementById('tools-list');
|
||||
const openSiteBtn = document.getElementById('open-site-btn');
|
||||
|
||||
// Protected site URL (should match config.js)
|
||||
const PROTECTED_SITE_URL = 'http://localhost:5173';
|
||||
|
||||
// State
|
||||
let currentUser = null;
|
||||
let tools = [];
|
||||
|
||||
// Initialize popup
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
async function init() {
|
||||
showState('loading');
|
||||
|
||||
try {
|
||||
// Check if service worker is available
|
||||
if (!chrome.runtime || !chrome.runtime.id) {
|
||||
throw new Error('Chrome Runtime nicht verfuegbar');
|
||||
}
|
||||
|
||||
// First, test if service worker is alive
|
||||
console.log('Testing service worker...');
|
||||
const pingResponse = await sendMessage({ action: 'PING' });
|
||||
console.log('PING response:', pingResponse);
|
||||
|
||||
if (!pingResponse) {
|
||||
throw new Error('Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.');
|
||||
}
|
||||
|
||||
if (!pingResponse.success) {
|
||||
throw new Error('Service Worker Fehler: ' + (pingResponse.error || 'Unbekannter Fehler'));
|
||||
}
|
||||
|
||||
console.log('Service Worker ist aktiv!');
|
||||
|
||||
console.log('Checking auth status...');
|
||||
|
||||
// Check current auth status
|
||||
const response = await sendMessage({ action: 'CHECK_AUTH' });
|
||||
|
||||
if (!response) {
|
||||
throw new Error('Keine Antwort vom Service Worker');
|
||||
}
|
||||
|
||||
console.log('Auth response:', response);
|
||||
|
||||
// If there's an error, show it but still allow login
|
||||
if (response.error && !response.success) {
|
||||
console.error('Service Worker Error:', response.error);
|
||||
showError('Hinweis: ' + response.error + ' - Login sollte trotzdem funktionieren.');
|
||||
}
|
||||
|
||||
if (response.success && response.authenticated) {
|
||||
currentUser = response.user;
|
||||
await loadTools();
|
||||
showLoggedInState();
|
||||
} else {
|
||||
// Not authenticated - show login form
|
||||
showState('login-form');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Init error:', error);
|
||||
showError('Fehler beim Laden: ' + error.message + '. Bitte Extension in chrome://extensions neu laden.');
|
||||
showState('login-form');
|
||||
}
|
||||
}
|
||||
|
||||
// Show a specific state (loading, login-form, tools-menu)
|
||||
function showState(stateId) {
|
||||
loadingEl.classList.add('hidden');
|
||||
loginFormEl.classList.add('hidden');
|
||||
toolsMenuEl.classList.add('hidden');
|
||||
|
||||
document.getElementById(stateId).classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Show logged in state with user info
|
||||
function showLoggedInState() {
|
||||
userEmailEl.textContent = currentUser.email || currentUser.name || 'Benutzer';
|
||||
renderTools();
|
||||
showState('tools-menu');
|
||||
}
|
||||
|
||||
// Send message to service worker
|
||||
function sendMessage(message) {
|
||||
return new Promise((resolve) => {
|
||||
// Add timeout to prevent hanging
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('Message timeout:', message.action);
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Service Worker antwortet nicht. Bitte Extension in chrome://extensions neu laden und Service Worker Konsole pruefen.'
|
||||
});
|
||||
}, 2000); // Reduced timeout to 2 seconds for faster feedback
|
||||
|
||||
try {
|
||||
if (!chrome.runtime || !chrome.runtime.id) {
|
||||
clearTimeout(timeout);
|
||||
resolve({ success: false, error: 'Chrome Runtime nicht verfuegbar' });
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage(message, (response) => {
|
||||
clearTimeout(timeout);
|
||||
if (chrome.runtime.lastError) {
|
||||
console.error('Chrome runtime error:', chrome.runtime.lastError);
|
||||
resolve({
|
||||
success: false,
|
||||
error: chrome.runtime.lastError.message || 'Service Worker Fehler'
|
||||
});
|
||||
} else {
|
||||
console.log('Response received:', response);
|
||||
resolve(response || { success: false, error: 'Keine Antwort vom Service Worker' });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
console.error('Send message error:', error);
|
||||
resolve({ success: false, error: error.message || 'Fehler beim Senden der Nachricht' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Login form submission
|
||||
authForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const email = emailInput.value.trim();
|
||||
const password = passwordInput.value;
|
||||
|
||||
if (!email || !password) {
|
||||
showError('Bitte E-Mail und Passwort eingeben');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginLoading(true);
|
||||
hideError();
|
||||
|
||||
const response = await sendMessage({
|
||||
action: 'LOGIN',
|
||||
email,
|
||||
password
|
||||
});
|
||||
|
||||
setLoginLoading(false);
|
||||
|
||||
if (response.success) {
|
||||
currentUser = response.user;
|
||||
await loadTools();
|
||||
showLoggedInState();
|
||||
|
||||
// Open protected site after successful login
|
||||
openProtectedSite();
|
||||
} else {
|
||||
showError(response.error || 'Anmeldung fehlgeschlagen');
|
||||
}
|
||||
});
|
||||
|
||||
// Logout button
|
||||
logoutBtn.addEventListener('click', async () => {
|
||||
const response = await sendMessage({ action: 'LOGOUT' });
|
||||
|
||||
if (response.success) {
|
||||
currentUser = null;
|
||||
tools = [];
|
||||
emailInput.value = '';
|
||||
passwordInput.value = '';
|
||||
showState('login-form');
|
||||
}
|
||||
});
|
||||
|
||||
// Open site button
|
||||
openSiteBtn.addEventListener('click', () => {
|
||||
openProtectedSite();
|
||||
});
|
||||
|
||||
// Open protected site in new tab
|
||||
function openProtectedSite() {
|
||||
chrome.tabs.create({ url: PROTECTED_SITE_URL });
|
||||
}
|
||||
|
||||
// Load tools settings
|
||||
async function loadTools() {
|
||||
const response = await sendMessage({ action: 'GET_SETTINGS' });
|
||||
if (response.success) {
|
||||
tools = response.tools;
|
||||
}
|
||||
}
|
||||
|
||||
// Render tools list
|
||||
function renderTools() {
|
||||
toolsListEl.innerHTML = '';
|
||||
|
||||
tools.forEach((tool, index) => {
|
||||
const toolEl = document.createElement('div');
|
||||
toolEl.className = 'tool-item';
|
||||
toolEl.innerHTML = `
|
||||
<div class="tool-header">
|
||||
<span class="tool-name">${escapeHtml(tool.name)}</span>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" data-tool-id="${tool.id}" ${tool.enabled ? 'checked' : ''}>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="tool-settings ${tool.enabled ? 'visible' : ''}" id="settings-${tool.id}">
|
||||
${renderToolSettings(tool)}
|
||||
</div>
|
||||
`;
|
||||
toolsListEl.appendChild(toolEl);
|
||||
|
||||
// Toggle event listener
|
||||
const toggle = toolEl.querySelector('input[type="checkbox"]');
|
||||
toggle.addEventListener('change', (e) => {
|
||||
handleToolToggle(tool.id, e.target.checked);
|
||||
});
|
||||
|
||||
// Settings change listeners
|
||||
const settingsInputs = toolEl.querySelectorAll('.setting-row input');
|
||||
settingsInputs.forEach(input => {
|
||||
input.addEventListener('change', (e) => {
|
||||
handleSettingChange(tool.id, e.target.dataset.setting, e.target.value);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Render settings inputs for a tool
|
||||
function renderToolSettings(tool) {
|
||||
if (!tool.settings) return '';
|
||||
|
||||
let html = '';
|
||||
for (const [key, value] of Object.entries(tool.settings)) {
|
||||
html += `
|
||||
<div class="setting-row">
|
||||
<label>${escapeHtml(formatSettingName(key))}</label>
|
||||
<input type="text" data-setting="${key}" value="${escapeHtml(value)}">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// Format setting key to readable name
|
||||
function formatSettingName(key) {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, str => str.toUpperCase())
|
||||
.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
// Handle tool toggle
|
||||
async function handleToolToggle(toolId, enabled) {
|
||||
const tool = tools.find(t => t.id === toolId);
|
||||
if (tool) {
|
||||
tool.enabled = enabled;
|
||||
|
||||
// Show/hide settings
|
||||
const settingsEl = document.getElementById(`settings-${toolId}`);
|
||||
if (settingsEl) {
|
||||
settingsEl.classList.toggle('visible', enabled);
|
||||
}
|
||||
|
||||
await saveTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle setting value change
|
||||
async function handleSettingChange(toolId, settingKey, value) {
|
||||
const tool = tools.find(t => t.id === toolId);
|
||||
if (tool && tool.settings) {
|
||||
tool.settings[settingKey] = value;
|
||||
await saveTools();
|
||||
}
|
||||
}
|
||||
|
||||
// Save tools to storage
|
||||
async function saveTools() {
|
||||
await sendMessage({ action: 'SAVE_SETTINGS', settings: tools });
|
||||
}
|
||||
|
||||
// Show error message
|
||||
function showError(message) {
|
||||
errorMessage.textContent = message;
|
||||
errorMessage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Hide error message
|
||||
function hideError() {
|
||||
errorMessage.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Set login button loading state
|
||||
function setLoginLoading(loading) {
|
||||
loginBtn.disabled = loading;
|
||||
loginBtn.querySelector('.btn-text').classList.toggle('hidden', loading);
|
||||
loginBtn.querySelector('.btn-spinner').classList.toggle('hidden', !loading);
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Reference in New Issue
Block a user