feat: Registrierung default AN, Einladungslinks, System-Email

- Registrierung ist standardmaessig aktiviert (erster User = Admin)
- Einmal-Registrierungslinks: Admin kann Links generieren die auch bei
  deaktivierter Registrierung funktionieren, nach Nutzung ungueltig
- Optional Link per System-Email versenden
- System-SMTP in Admin-Einstellungen konfigurierbar:
  Server, Port, SSL, Benutzername, Passwort, Absender-Adresse
- SMTP-Verbindungstest-Button
- Register-Seite akzeptiert ?invite=TOKEN aus der URL

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 15:41:42 +02:00
parent 042a067e81
commit 113fe7140f
5 changed files with 278 additions and 12 deletions
+17 -3
View File
@@ -76,7 +76,7 @@ def registration_status():
"""Check if public registration is allowed."""
from app.models.settings import AppSettings
is_first_user = User.query.count() == 0
public_registration = AppSettings.get_bool('public_registration', default=False)
public_registration = AppSettings.get_bool('public_registration', default=True)
return jsonify({
'allowed': is_first_user or public_registration,
'is_first_user': is_first_user,
@@ -87,9 +87,19 @@ def registration_status():
def register():
from app.models.settings import AppSettings
# Check if registration is allowed
is_first_user = User.query.count() == 0
if not is_first_user and not AppSettings.get_bool('public_registration', default=False):
# Check invite token (works even if public registration is off)
invite_token = request.args.get('invite') or (request.get_json() or {}).get('invite_token')
valid_invite = False
if invite_token:
from app.models.settings import AppSettings as _S
stored = _S.get(f'invite_{invite_token}', '')
if stored == 'valid':
valid_invite = True
# Check if registration is allowed
if not is_first_user and not valid_invite and not AppSettings.get_bool('public_registration', default=True):
return jsonify({'error': 'Oeffentliche Registrierung ist deaktiviert'}), 403
data = request.get_json()
@@ -129,6 +139,10 @@ def register():
db.session.add(user)
db.session.commit()
# Invalidate invite token if used
if valid_invite and invite_token:
AppSettings.set(f'invite_{invite_token}', 'used')
access_token = create_access_token(user.id)
refresh_token = create_refresh_token(user.id)
+108 -1
View File
@@ -138,7 +138,13 @@ def delete_user(user_id):
@admin_required
def get_settings():
return jsonify({
'public_registration': AppSettings.get_bool('public_registration', default=False),
'public_registration': AppSettings.get_bool('public_registration', default=True),
'system_smtp_host': AppSettings.get('system_smtp_host', ''),
'system_smtp_port': int(AppSettings.get('system_smtp_port', '587')),
'system_smtp_ssl': AppSettings.get_bool('system_smtp_ssl', default=True),
'system_smtp_username': AppSettings.get('system_smtp_username', ''),
'system_smtp_password_set': bool(AppSettings.get('system_smtp_password', '')),
'system_email_from': AppSettings.get('system_email_from', ''),
}), 200
@@ -148,9 +154,110 @@ def update_settings():
data = request.get_json()
if 'public_registration' in data:
AppSettings.set('public_registration', str(data['public_registration']).lower())
for key in ['system_smtp_host', 'system_smtp_port', 'system_smtp_ssl',
'system_smtp_username', 'system_email_from']:
if key in data:
AppSettings.set(key, str(data[key]))
if 'system_smtp_password' in data and data['system_smtp_password']:
AppSettings.set('system_smtp_password', data['system_smtp_password'])
return jsonify({'message': 'Einstellungen gespeichert'}), 200
@api_bp.route('/settings/test-email', methods=['POST'])
@admin_required
def test_system_email():
"""Test system SMTP connection."""
import smtplib
host = AppSettings.get('system_smtp_host', '')
port = int(AppSettings.get('system_smtp_port', '587'))
ssl = AppSettings.get_bool('system_smtp_ssl', default=True)
username = AppSettings.get('system_smtp_username', '')
password = AppSettings.get('system_smtp_password', '')
from_addr = AppSettings.get('system_email_from', '')
if not host or not username or not password:
return jsonify({'error': 'SMTP-Einstellungen unvollstaendig'}), 400
try:
if ssl and port == 465:
server = smtplib.SMTP_SSL(host, port, timeout=10)
else:
server = smtplib.SMTP(host, port, timeout=10)
server.starttls()
server.login(username, password)
server.quit()
return jsonify({'message': 'SMTP-Verbindung erfolgreich'}), 200
except Exception as e:
return jsonify({'error': f'Verbindungsfehler: {str(e)}'}), 400
# --- Invite Links ---
@api_bp.route('/settings/invite', methods=['POST'])
@admin_required
def create_invite_link():
"""Generate a one-time registration link."""
import secrets
token = secrets.token_urlsafe(32)
AppSettings.set(f'invite_{token}', 'valid')
data = request.get_json() or {}
send_email = data.get('send_to_email', '')
result = {
'token': token,
'url': f'/register?invite={token}',
}
# Optionally send via system email
if send_email:
import smtplib
from email.mime.text import MIMEText
host = AppSettings.get('system_smtp_host', '')
port = int(AppSettings.get('system_smtp_port', '587'))
ssl = AppSettings.get_bool('system_smtp_ssl', default=True)
username = AppSettings.get('system_smtp_username', '')
password = AppSettings.get('system_smtp_password', '')
from_addr = AppSettings.get('system_email_from', '')
if not host or not password:
return jsonify({**result, 'email_sent': False,
'email_error': 'System-Email nicht konfiguriert'}), 200
# Build full URL from request
base_url = request.host_url.rstrip('/')
full_url = f'{base_url}/register?invite={token}'
body = (
f'Du wurdest zur Mini-Cloud eingeladen!\n\n'
f'Klicke auf folgenden Link, um dich zu registrieren:\n'
f'{full_url}\n\n'
f'Dieser Link ist nur einmal verwendbar.'
)
msg = MIMEText(body, 'plain', 'utf-8')
msg['From'] = from_addr or username
msg['To'] = send_email
msg['Subject'] = 'Einladung zur Mini-Cloud'
try:
if ssl and port == 465:
server = smtplib.SMTP_SSL(host, port, timeout=10)
else:
server = smtplib.SMTP(host, port, timeout=10)
server.starttls()
server.login(username, password)
server.sendmail(from_addr or username, [send_email], msg.as_string())
server.quit()
result['email_sent'] = True
except Exception as e:
result['email_sent'] = False
result['email_error'] = str(e)
return jsonify(result), 201
# --- User search (for sharing dialogs) ---
@api_bp.route('/users/search', methods=['GET'])