import uuid from datetime import datetime, timezone from flask import request, jsonify from app.api import api_bp from app.api.auth import token_required from app.extensions import db from app.models.contact import AddressBook, Contact, AddressBookShare from app.models.user import User def _get_addressbook_or_err(book_id, user, need_write=False): book = db.session.get(AddressBook, book_id) if not book: return None, (jsonify({'error': 'Adressbuch nicht gefunden'}), 404) if book.owner_id == user.id: return book, None share = AddressBookShare.query.filter_by( address_book_id=book_id, shared_with_id=user.id ).first() if not share: return None, (jsonify({'error': 'Zugriff verweigert'}), 403) if need_write and share.permission != 'readwrite': return None, (jsonify({'error': 'Schreibzugriff verweigert'}), 403) return book, None # --- Address Books --- @api_bp.route('/addressbooks', methods=['GET']) @token_required def list_addressbooks(): user = request.current_user own = AddressBook.query.filter_by(owner_id=user.id).all() shared_ids = [s.address_book_id for s in AddressBookShare.query.filter_by(shared_with_id=user.id).all()] shared = AddressBook.query.filter(AddressBook.id.in_(shared_ids)).all() if shared_ids else [] result = [] for b in own: d = b.to_dict() d['permission'] = 'owner' d['contact_count'] = b.contacts.count() result.append(d) for b in shared: d = b.to_dict() share = AddressBookShare.query.filter_by( address_book_id=b.id, shared_with_id=user.id ).first() d['permission'] = share.permission if share else 'read' d['owner_name'] = b.owner.username d['contact_count'] = b.contacts.count() result.append(d) return jsonify(result), 200 @api_bp.route('/addressbooks', methods=['POST']) @token_required def create_addressbook(): user = request.current_user data = request.get_json() name = data.get('name', '').strip() if not name: return jsonify({'error': 'Name erforderlich'}), 400 book = AddressBook(owner_id=user.id, name=name, description=data.get('description', '')) db.session.add(book) db.session.commit() return jsonify(book.to_dict()), 201 @api_bp.route('/addressbooks/', methods=['PUT']) @token_required def update_addressbook(book_id): user = request.current_user book = db.session.get(AddressBook, book_id) if not book or book.owner_id != user.id: return jsonify({'error': 'Nicht gefunden'}), 404 data = request.get_json() if 'name' in data: book.name = data['name'].strip() if 'description' in data: book.description = data['description'] db.session.commit() return jsonify(book.to_dict()), 200 @api_bp.route('/addressbooks/', methods=['DELETE']) @token_required def delete_addressbook(book_id): user = request.current_user book = db.session.get(AddressBook, book_id) if not book or book.owner_id != user.id: return jsonify({'error': 'Nicht gefunden'}), 404 db.session.delete(book) db.session.commit() return jsonify({'message': 'Adressbuch geloescht'}), 200 # --- Contacts --- @api_bp.route('/addressbooks//contacts', methods=['GET']) @token_required def list_contacts(book_id): user = request.current_user book, err = _get_addressbook_or_err(book_id, user) if err: return err search = request.args.get('search', '').strip() query = Contact.query.filter_by(address_book_id=book_id) if search: query = query.filter(Contact.display_name.ilike(f'%{search}%')) contacts = query.order_by(Contact.display_name).all() return jsonify([c.to_dict() for c in contacts]), 200 @api_bp.route('/addressbooks//contacts', methods=['POST']) @token_required def create_contact(book_id): user = request.current_user book, err = _get_addressbook_or_err(book_id, user, need_write=True) if err: return err data = request.get_json() display_name = data.get('display_name', '').strip() if not display_name: return jsonify({'error': 'Name erforderlich'}), 400 contact_uid = str(uuid.uuid4()) email = data.get('email', '') phone = data.get('phone', '') org = data.get('organization', '') notes = data.get('notes', '') vcard = _build_vcard(contact_uid, display_name, email, phone, org, notes) contact = Contact( address_book_id=book_id, uid=contact_uid, vcard_data=vcard, display_name=display_name, email=email or None, phone=phone or None, ) db.session.add(contact) db.session.commit() return jsonify(contact.to_dict()), 201 @api_bp.route('/contacts/', methods=['GET']) @token_required def get_contact(contact_id): user = request.current_user contact = db.session.get(Contact, contact_id) if not contact: return jsonify({'error': 'Kontakt nicht gefunden'}), 404 book, err = _get_addressbook_or_err(contact.address_book_id, user) if err: return err result = contact.to_dict() result['vcard_data'] = contact.vcard_data return jsonify(result), 200 @api_bp.route('/contacts/', methods=['PUT']) @token_required def update_contact(contact_id): user = request.current_user contact = db.session.get(Contact, contact_id) if not contact: return jsonify({'error': 'Kontakt nicht gefunden'}), 404 book, err = _get_addressbook_or_err(contact.address_book_id, user, need_write=True) if err: return err data = request.get_json() if 'display_name' in data: contact.display_name = data['display_name'].strip() if 'email' in data: contact.email = data['email'] or None if 'phone' in data: contact.phone = data['phone'] or None contact.vcard_data = _build_vcard( contact.uid, contact.display_name, data.get('email', contact.email or ''), data.get('phone', contact.phone or ''), data.get('organization', ''), data.get('notes', ''), ) contact.updated_at = datetime.now(timezone.utc) db.session.commit() return jsonify(contact.to_dict()), 200 @api_bp.route('/contacts/', methods=['DELETE']) @token_required def delete_contact(contact_id): user = request.current_user contact = db.session.get(Contact, contact_id) if not contact: return jsonify({'error': 'Kontakt nicht gefunden'}), 404 book, err = _get_addressbook_or_err(contact.address_book_id, user, need_write=True) if err: return err db.session.delete(contact) db.session.commit() return jsonify({'message': 'Kontakt geloescht'}), 200 # --- Sharing --- @api_bp.route('/addressbooks//share', methods=['POST']) @token_required def share_addressbook(book_id): user = request.current_user book = db.session.get(AddressBook, book_id) if not book or book.owner_id != user.id: return jsonify({'error': 'Nur der Eigentuemer kann teilen'}), 403 data = request.get_json() username = data.get('username', '').strip() permission = data.get('permission', 'read') if permission not in ('read', 'readwrite'): return jsonify({'error': 'Ungueltige Berechtigung'}), 400 target = User.query.filter_by(username=username).first() if not target: return jsonify({'error': 'Benutzer nicht gefunden'}), 404 if target.id == user.id: return jsonify({'error': 'Kann nicht mit sich selbst teilen'}), 400 existing = AddressBookShare.query.filter_by( address_book_id=book_id, shared_with_id=target.id ).first() is_new = not existing if existing: existing.permission = permission else: share = AddressBookShare( address_book_id=book_id, shared_with_id=target.id, permission=permission ) db.session.add(share) db.session.commit() if is_new: try: from app.services.system_mail import notify_contacts_shared notify_contacts_shared(book.name, user.username, target, permission) except Exception: pass return jsonify({'message': f'Adressbuch mit {username} geteilt'}), 200 @api_bp.route('/addressbooks//shares', methods=['GET']) @token_required def list_addressbook_shares(book_id): user = request.current_user book = db.session.get(AddressBook, book_id) if not book or book.owner_id != user.id: return jsonify({'error': 'Nicht gefunden'}), 404 shares = AddressBookShare.query.filter_by(address_book_id=book_id).all() return jsonify([{ 'id': s.id, 'user_id': s.shared_with_id, 'username': s.shared_with.username, 'permission': s.permission, } for s in shares]), 200 @api_bp.route('/addressbooks//shares/', methods=['DELETE']) @token_required def remove_addressbook_share(book_id, share_id): user = request.current_user book = db.session.get(AddressBook, book_id) if not book or book.owner_id != user.id: return jsonify({'error': 'Nicht gefunden'}), 404 share = db.session.get(AddressBookShare, share_id) if not share or share.address_book_id != book_id: return jsonify({'error': 'Freigabe nicht gefunden'}), 404 db.session.delete(share) db.session.commit() return jsonify({'message': 'Freigabe entfernt'}), 200 # --- Import/Export --- @api_bp.route('/addressbooks//export', methods=['GET']) @token_required def export_contacts(book_id): user = request.current_user book, err = _get_addressbook_or_err(book_id, user) if err: return err contacts = Contact.query.filter_by(address_book_id=book_id).all() vcards = '\r\n'.join(c.vcard_data for c in contacts) from flask import Response return Response( vcards, mimetype='text/vcard', headers={'Content-Disposition': f'attachment; filename="{book.name}.vcf"'}, ) # --- Helpers --- def _build_vcard(uid, display_name, email='', phone='', org='', notes=''): parts = display_name.split(' ', 1) first = parts[0] last = parts[1] if len(parts) > 1 else '' lines = [ 'BEGIN:VCARD', 'VERSION:3.0', f'UID:{uid}', f'FN:{display_name}', f'N:{last};{first};;;', ] if email: lines.append(f'EMAIL:{email}') if phone: lines.append(f'TEL:{phone}') if org: lines.append(f'ORG:{org}') if notes: lines.append(f'NOTE:{notes}') lines.append(f'REV:{datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")}') lines.append('END:VCARD') return '\r\n'.join(lines)