diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py index 8aafca0..8f2f77c 100644 --- a/backend/app/api/auth.py +++ b/backend/app/api/auth.py @@ -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) diff --git a/backend/app/api/users.py b/backend/app/api/users.py index cd947a8..1cb4c12 100644 --- a/backend/app/api/users.py +++ b/backend/app/api/users.py @@ -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']) diff --git a/frontend/src/stores/auth.js b/frontend/src/stores/auth.js index 8c5a795..9958d05 100644 --- a/frontend/src/stores/auth.js +++ b/frontend/src/stores/auth.js @@ -19,9 +19,10 @@ export const useAuthStore = defineStore('auth', () => { return response.data } - async function register(username, password, email) { + async function register(username, password, email, inviteToken) { const payload = { username, password } if (email) payload.email = email + if (inviteToken) payload.invite_token = inviteToken const response = await apiClient.post('/auth/register', payload) user.value = response.data.user accessToken.value = response.data.access_token diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index a58694e..be1ba94 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -6,14 +6,73 @@
Wenn aktiviert, koennen sich neue Benutzer selbst registrieren. Andernfalls kann nur ein Admin neue Benutzer anlegen.
+Wenn aktiviert, koennen sich neue Benutzer selbst registrieren. Andernfalls kann nur ein Admin neue Benutzer anlegen oder Einladungslinks versenden.
Einmal-Links funktionieren auch bei deaktivierter oeffentlicher Registrierung.
+{{ fullInviteUrl }}
+
+ Wird fuer Einladungslinks und System-Benachrichtigungen verwendet.
+