Files
minmal-file-cloud-email-pim…/backend/app/api/users.py
T
Stefan Hacker 62f550c373 feat: Mini-Cloud Plattform - komplette Implementierung Phase 0-8
Selbstgehostete Web-Cloud mit Dateiverwaltung, Kalender, Kontakte,
Email-Webclient, Office-Viewer und Passwort-Manager.

Backend (Flask/Python):
- JWT-Auth mit Access/Refresh Tokens, Benutzerverwaltung
- Dateien: Upload/Download, Ordner, Berechtigungen, Share-Links
- Kalender: CRUD, Teilen, iCal-Export, CalDAV well-known URLs
- Kontakte: Adressbuecher, vCard-Export, Teilen
- Email: IMAP/SMTP-Proxy, Multi-Account
- Office-Viewer: DOCX/XLSX/PPTX/PDF Vorschau
- Passwort-Manager: AES-256-GCM clientseitig, KeePass-Import
- Sync-API fuer Desktop/Mobile-Clients
- SQLite mit WAL-Modus

Frontend (Vue 3 + PrimeVue):
- Datei-Explorer mit Breadcrumbs und Share-Dialogen
- Monatskalender mit Event-Verwaltung
- Kontaktliste mit Adressbuch-Sidebar
- Email-Client mit 3-Spalten-Layout
- Passwort-Manager mit TOTP und Passwort-Generator
- Admin-Panel, Settings, oeffentliche Share-Seite

Docker: Multi-Stage Build, Bind Mounts (keine Volumes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:53:28 +02:00

112 lines
3.8 KiB
Python

from flask import request, jsonify
from app.api import api_bp
from app.api.auth import admin_required, token_required
from app.extensions import db
from app.models.user import User
@api_bp.route('/users', methods=['GET'])
@admin_required
def list_users():
users = User.query.order_by(User.created_at.desc()).all()
return jsonify([u.to_dict(include_email=True) for u in users]), 200
@api_bp.route('/users/<int:user_id>', methods=['GET'])
@admin_required
def get_user(user_id):
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': 'Benutzer nicht gefunden'}), 404
return jsonify(user.to_dict(include_email=True)), 200
@api_bp.route('/users/<int:user_id>', methods=['PUT'])
@admin_required
def update_user(user_id):
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': 'Benutzer nicht gefunden'}), 404
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten gesendet'}), 400
if 'email' in data:
email = data['email'].strip() or None
if email and email != user.email:
if User.query.filter_by(email=email).first():
return jsonify({'error': 'Email bereits vergeben'}), 409
user.email = email
if 'role' in data and data['role'] in ('admin', 'user'):
# Prevent removing last admin
if user.role == 'admin' and data['role'] == 'user':
admin_count = User.query.filter_by(role='admin', is_active=True).count()
if admin_count <= 1:
return jsonify({'error': 'Letzter Admin kann nicht herabgestuft werden'}), 400
user.role = data['role']
if 'is_active' in data:
if user.id == request.current_user.id and not data['is_active']:
return jsonify({'error': 'Eigenes Konto kann nicht deaktiviert werden'}), 400
user.is_active = data['is_active']
if 'storage_quota_mb' in data:
user.storage_quota_mb = max(0, int(data['storage_quota_mb']))
if 'password' in data and data['password']:
if len(data['password']) < 8:
return jsonify({'error': 'Passwort muss mindestens 8 Zeichen lang sein'}), 400
user.set_password(data['password'])
db.session.commit()
return jsonify(user.to_dict(include_email=True)), 200
@api_bp.route('/users/<int:user_id>', methods=['DELETE'])
@admin_required
def delete_user(user_id):
user = db.session.get(User, user_id)
if not user:
return jsonify({'error': 'Benutzer nicht gefunden'}), 404
if user.id == request.current_user.id:
return jsonify({'error': 'Eigenes Konto kann nicht geloescht werden'}), 400
if user.role == 'admin':
admin_count = User.query.filter_by(role='admin', is_active=True).count()
if admin_count <= 1:
return jsonify({'error': 'Letzter Admin kann nicht geloescht werden'}), 400
db.session.delete(user)
db.session.commit()
return jsonify({'message': 'Benutzer geloescht'}), 200
@api_bp.route('/auth/change-password', methods=['POST'])
@token_required
def change_password():
data = request.get_json()
if not data:
return jsonify({'error': 'Keine Daten gesendet'}), 400
current_password = data.get('current_password', '')
new_password = data.get('new_password', '')
if not current_password or not new_password:
return jsonify({'error': 'Aktuelles und neues Passwort erforderlich'}), 400
if len(new_password) < 8:
return jsonify({'error': 'Neues Passwort muss mindestens 8 Zeichen lang sein'}), 400
user = request.current_user
if not user.check_password(current_password):
return jsonify({'error': 'Aktuelles Passwort falsch'}), 401
user.set_password(new_password)
db.session.commit()
return jsonify({'message': 'Passwort geaendert'}), 200