From 4d67819cac63862a2f4b6fcffde36d36efc66598 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Tue, 14 Apr 2026 15:34:22 +0200 Subject: [PATCH] feat: Vor-/Nachname, geteilte Listen zeigen Eigentuemer Backend: - User.first_name / User.last_name (nullable, Auto-Migrate fuegt sie an) full_name/display_name als Properties + in to_dict - TaskList.owner-Relationship ergaenzt (fehlte, daher wurden geteilte Listen beim Empfaenger nicht korrekt aufgeloest) - /auth/me GET + PUT (Profil bearbeiten: Vorname, Nachname, E-Mail) - /users/search findet jetzt auch nach Vor-/Nachname und liefert full_name/display_name mit - list_tasklists/list_calendars/list_addressbooks liefern owner_full_name und owner_display_name Frontend: - Sidebars bei Kontakten/Kalender/Aufgaben: "(geteilt von )" mit Fallback auf Username - User-Search-Popup zeigt vollen Namen neben Username - SettingsView: Vorname/Nachname/E-Mail bearbeiten Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/calendar.py | 2 + backend/app/api/contacts.py | 2 + backend/app/api/tasks.py | 5 ++- backend/app/api/users.py | 35 ++++++++++++++++- backend/app/models/user.py | 18 +++++++++ frontend/src/views/CalendarView.vue | 10 ++++- frontend/src/views/ContactsView.vue | 10 ++++- frontend/src/views/SettingsView.vue | 59 +++++++++++++++++++++++++++-- frontend/src/views/TasksView.vue | 10 ++++- 9 files changed, 138 insertions(+), 13 deletions(-) diff --git a/backend/app/api/calendar.py b/backend/app/api/calendar.py index a40cc57..67cc398 100644 --- a/backend/app/api/calendar.py +++ b/backend/app/api/calendar.py @@ -109,6 +109,8 @@ def list_calendars(): if share and share.color: d['color'] = share.color d['owner_name'] = c.owner.username + d['owner_full_name'] = c.owner.full_name + d['owner_display_name'] = c.owner.display_name result.append(d) return jsonify(result), 200 diff --git a/backend/app/api/contacts.py b/backend/app/api/contacts.py index a47712e..259c59f 100644 --- a/backend/app/api/contacts.py +++ b/backend/app/api/contacts.py @@ -292,6 +292,8 @@ def list_addressbooks(): if share and share.color: d['color'] = share.color d['owner_name'] = b.owner.username + d['owner_full_name'] = b.owner.full_name + d['owner_display_name'] = b.owner.display_name d['contact_count'] = b.contacts.count() result.append(d) diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 8e912c4..0df727f 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -212,7 +212,10 @@ def list_tasklists(): continue d = tl.to_dict() d['permission'] = s.permission - d['owner_name'] = tl.owner.username if tl.owner else '' + owner = tl.owner + d['owner_name'] = owner.username if owner else '' + d['owner_full_name'] = owner.full_name if owner else '' + d['owner_display_name'] = owner.display_name if owner else '' d['task_count'] = tl.tasks.count() if s.color: d['color'] = s.color diff --git a/backend/app/api/users.py b/backend/app/api/users.py index d44856a..744a2fb 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -281,6 +281,31 @@ def create_invite_link(): # --- User search (for sharing dialogs) --- +@api_bp.route('/auth/me', methods=['GET']) +@token_required +def get_me(): + return jsonify(request.current_user.to_dict(include_email=True)), 200 + + +@api_bp.route('/auth/me', methods=['PUT']) +@token_required +def update_me(): + user = request.current_user + data = request.get_json() or {} + if 'first_name' in data: + user.first_name = (data.get('first_name') or '').strip() or None + if 'last_name' in data: + user.last_name = (data.get('last_name') or '').strip() or None + if 'email' in data: + email = (data.get('email') or '').strip() or None + if email and email != user.email: + if User.query.filter(User.email == email, User.id != user.id).first(): + return jsonify({'error': 'E-Mail ist bereits vergeben'}), 409 + user.email = email + db.session.commit() + return jsonify(user.to_dict(include_email=True)), 200 + + @api_bp.route('/users/search', methods=['GET']) @token_required def search_users(): @@ -289,13 +314,19 @@ def search_users(): if len(query) < 2: return jsonify([]), 200 + like = f'%{query}%' users = User.query.filter( - User.username.ilike(f'%{query}%'), + (User.username.ilike(like)) | (User.first_name.ilike(like)) | (User.last_name.ilike(like)), User.id != request.current_user.id, User.is_active == True, ).limit(10).all() - return jsonify([{'id': u.id, 'username': u.username} for u in users]), 200 + return jsonify([{ + 'id': u.id, + 'username': u.username, + 'full_name': u.full_name, + 'display_name': u.display_name, + } for u in users]), 200 # --- Change password (non-admin, own account) --- diff --git a/backend/app/models/user.py b/backend/app/models/user.py index c1e1256..400cb40 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -9,6 +9,8 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False, index=True) email = db.Column(db.String(255), unique=True, nullable=True) + first_name = db.Column(db.String(100), nullable=True) + last_name = db.Column(db.String(100), nullable=True) password_hash = db.Column(db.String(255), nullable=False) role = db.Column(db.String(20), default='user', nullable=False) # 'admin' or 'user' master_key_salt = db.Column(db.LargeBinary, nullable=True) # For password manager @@ -23,6 +25,7 @@ class User(db.Model): foreign_keys='File.owner_id') calendars = db.relationship('Calendar', backref='owner', lazy='dynamic') address_books = db.relationship('AddressBook', backref='owner', lazy='dynamic') + task_lists = db.relationship('TaskList', backref='owner', lazy='dynamic') email_accounts = db.relationship('EmailAccount', backref='user', lazy='dynamic', order_by='EmailAccount.sort_order') password_folders = db.relationship('PasswordFolder', backref='owner', lazy='dynamic') @@ -33,10 +36,25 @@ class User(db.Model): def check_password(self, password): return bcrypt.check_password_hash(self.password_hash, password) + @property + def full_name(self) -> str: + """Vor- + Nachname zusammengesetzt, sonst Leerstring.""" + parts = [self.first_name or '', self.last_name or ''] + return ' '.join(p.strip() for p in parts if p and p.strip()) + + @property + def display_name(self) -> str: + """Voller Name falls vorhanden, sonst Username.""" + return self.full_name or self.username + def to_dict(self, include_email=False): data = { 'id': self.id, 'username': self.username, + 'first_name': self.first_name or '', + 'last_name': self.last_name or '', + 'full_name': self.full_name, + 'display_name': self.display_name, 'role': self.role, 'is_active': self.is_active, 'storage_quota_mb': self.storage_quota_mb, diff --git a/frontend/src/views/CalendarView.vue b/frontend/src/views/CalendarView.vue index 7319559..011abac 100644 --- a/frontend/src/views/CalendarView.vue +++ b/frontend/src/views/CalendarView.vue @@ -20,7 +20,10 @@
{{ cal.name }} - (geteilt) + + (geteilt von {{ cal.owner_display_name || cal.owner_name }}) +