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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user