feat: Passwort-Import aus Firefox, Chrome, Bitwarden und generischem CSV

- Firefox-Import: CSV aus Einstellungen > Passwoerter > Exportieren
  Domain wird automatisch als Titel extrahiert
- Generischer CSV-Import: Erkennt automatisch Spaltennamen aus
  Chrome, Bitwarden, 1Password und anderen Managern
- KeePass-Import bleibt bestehen
- Einheitlicher Import-Dialog mit Quellen-Auswahl (Dropdown)
- Jede Quelle zeigt eine kurze Anleitung an
- Alle Eintraege werden clientseitig verschluesselt vor dem Speichern
- Backend: /passwords/import/firefox und /passwords/import/csv Endpunkte

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 17:42:00 +02:00
parent 7220a2ef75
commit 0150bf4b2f
2 changed files with 238 additions and 28 deletions
+153
View File
@@ -359,3 +359,156 @@ def import_keepass():
except Exception as e:
return jsonify({'error': f'Import fehlgeschlagen: {str(e)}'}), 400
# --- Firefox CSV Import ---
@api_bp.route('/passwords/import/firefox', methods=['POST'])
@token_required
def import_firefox():
"""Import passwords from Firefox CSV export.
Firefox: Einstellungen > Passwoerter > ... > Passwoerter exportieren
CSV columns: url, username, password, httpRealm, formActionOrigin, guid, timeCreated, timeLastUsed, timePasswordChanged
"""
user = request.current_user
if 'file' not in request.files:
return jsonify({'error': 'Keine Datei gesendet'}), 400
csv_file = request.files['file']
try:
import csv
import io
content = csv_file.read().decode('utf-8')
reader = csv.DictReader(io.StringIO(content))
entries = []
for row in reader:
url = row.get('url', row.get('origin', '')).strip()
username = row.get('username', '').strip()
password = row.get('password', '').strip()
if not url and not username and not password:
continue
# Extract domain as title
title = url
try:
from urllib.parse import urlparse
parsed = urlparse(url)
title = parsed.netloc or parsed.path or url
except Exception:
pass
entries.append({
'title': title,
'url': url,
'username': username,
'password': password,
'notes': '',
'totp': '',
'group': 'Firefox Import',
})
return jsonify({
'entries': entries,
'groups': [{'name': 'Firefox Import', 'uuid': 'firefox-import', 'parent_uuid': None}] if entries else [],
'count': len(entries),
}), 200
except Exception as e:
return jsonify({'error': f'CSV-Import fehlgeschlagen: {str(e)}'}), 400
# --- Generic CSV Import (Chrome, Bitwarden, etc.) ---
@api_bp.route('/passwords/import/csv', methods=['POST'])
@token_required
def import_generic_csv():
"""Import passwords from generic CSV (Chrome, Bitwarden, 1Password, etc.)
Tries to auto-detect columns: name/title, url, username, password, notes
"""
user = request.current_user
if 'file' not in request.files:
return jsonify({'error': 'Keine Datei gesendet'}), 400
csv_file = request.files['file']
try:
import csv
import io
content = csv_file.read().decode('utf-8')
reader = csv.DictReader(io.StringIO(content))
# Map common column names
col_map = {
'title': ['title', 'name', 'titel', 'bezeichnung', 'entry'],
'url': ['url', 'uri', 'website', 'login_uri', 'origin'],
'username': ['username', 'user', 'login', 'benutzername', 'email', 'login_username'],
'password': ['password', 'passwort', 'pass', 'login_password'],
'notes': ['notes', 'note', 'notizen', 'comment', 'comments', 'extra'],
'totp': ['totp', 'otp', 'login_totp', '2fa'],
'group': ['group', 'folder', 'ordner', 'category', 'kategorie', 'type'],
}
def find_col(fieldnames, target):
for col_name in col_map.get(target, []):
for fn in fieldnames:
if fn.lower().strip() == col_name:
return fn
return None
fieldnames = reader.fieldnames or []
mapping = {target: find_col(fieldnames, target) for target in col_map}
entries = []
groups_set = set()
for row in reader:
def get(target):
col = mapping.get(target)
return row.get(col, '').strip() if col else ''
title = get('title')
url = get('url')
username = get('username')
password = get('password')
if not title and not url and not username and not password:
continue
if not title and url:
try:
from urllib.parse import urlparse
title = urlparse(url).netloc or url
except Exception:
title = url
group = get('group') or 'CSV Import'
groups_set.add(group)
entries.append({
'title': title or '(Unbenannt)',
'url': url,
'username': username,
'password': password,
'notes': get('notes'),
'totp': get('totp'),
'group': group,
})
groups = [{'name': g, 'uuid': g, 'parent_uuid': None} for g in groups_set]
return jsonify({
'entries': entries,
'groups': groups,
'count': len(entries),
}), 200
except Exception as e:
return jsonify({'error': f'CSV-Import fehlgeschlagen: {str(e)}'}), 400