Trigger vulnerability scanning to generate data for threat intelligence enrichment
()
| 8014 | |
| 8015 | @app.route('/api/auth/setup', methods=['POST']) |
| 8016 | def auth_setup(): |
| 8017 | """Initial authentication setup - creates user, encrypts DB, generates recovery codes.""" |
| 8018 | if auth_mgr.is_configured(): |
| 8019 | return jsonify({'success': False, 'error': 'Authentication is already configured'}), 400 |
| 8020 | |
| 8021 | data = request.get_json() |
| 8022 | if not data: |
| 8023 | return jsonify({'success': False, 'error': 'No data provided'}), 400 |
| 8024 | |
| 8025 | username = data.get('username', '').strip() |
| 8026 | password = data.get('password', '') |
| 8027 | confirm = data.get('confirm_password', '') |
| 8028 | |
| 8029 | if password != confirm: |
| 8030 | return jsonify({'success': False, 'error': 'Passwords do not match'}), 400 |
| 8031 | |
| 8032 | result = auth_mgr.setup(username, password) |
| 8033 | if result['success']: |
| 8034 | # Auto-login after setup |
| 8035 | session['authenticated'] = True |
| 8036 | session['username'] = username |
| 8037 | session['login_time'] = time.time() |
| 8038 | session.permanent = True |
| 8039 | return jsonify(result) |
| 8040 | else: |
| 8041 | return jsonify(result), 400 |
| 8042 | |
| 8043 | |
| 8044 | # Simple in-memory rate limit for login / recovery. Per source-IP sliding window: |
| 8045 | # _AUTH_LOCKOUT_THRESHOLD failed attempts within _AUTH_LOCKOUT_WINDOW seconds |
| 8046 | # → next attempts rejected for _AUTH_LOCKOUT_WINDOW seconds. |
| 8047 | # Memory only: resets on process restart. Sufficient on a single-Pi deployment. |
| 8048 | _AUTH_LOCKOUT_THRESHOLD = 5 |
| 8049 | _AUTH_LOCKOUT_WINDOW = 60 |
| 8050 | _auth_attempts = {} # ip -> list[timestamp] of failed attempts |
| 8051 | _auth_attempts_lock = threading.Lock() |
| 8052 | |
| 8053 | |
| 8054 | def _auth_client_ip(): |
| 8055 | # Trust X-Forwarded-For only if explicitly configured; otherwise use remote_addr. |
| 8056 | return request.headers.get('X-Real-IP') or request.remote_addr or 'unknown' |
| 8057 | |
| 8058 | |
| 8059 | def _auth_rate_limited(ip): |
| 8060 | now = time.time() |
| 8061 | with _auth_attempts_lock: |
| 8062 | attempts = [t for t in _auth_attempts.get(ip, []) if now - t < _AUTH_LOCKOUT_WINDOW] |
| 8063 | _auth_attempts[ip] = attempts |
| 8064 | if len(attempts) >= _AUTH_LOCKOUT_THRESHOLD: |
| 8065 | retry_after = int(_AUTH_LOCKOUT_WINDOW - (now - attempts[0])) + 1 |
| 8066 | return True, max(retry_after, 1) |
| 8067 | return False, 0 |
| 8068 | |
| 8069 | |
| 8070 | def _auth_record_failure(ip): |
| 8071 | with _auth_attempts_lock: |
| 8072 | _auth_attempts.setdefault(ip, []).append(time.time()) |
| 8073 |
nothing calls this directly
no test coverage detected