Coverage for integrations / social / api_regional_host.py: 15.6%
173 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2Regional Host API - Blueprint for regional host request + approval.
4POST /api/social/regional-host/request - User requests regional host status
5GET /api/social/regional-host/requests - Steward lists pending requests
6POST /api/social/regional-host/approve - Steward approves
7POST /api/social/regional-host/reject — Steward rejects
8POST /api/social/regional-host/revoke — Steward revokes
9GET /api/social/regional-host/status — User checks their request status
10GET /api/social/regional-host/capacity — Region capacity metrics (public)
11GET /api/social/regional-host/rebalance — Elastic rebalance suggestions (admin)
12GET /api/social/regional-host/scaling — Horizontal scaling check (admin)
13GET /api/social/regional-host/eligibility — User eligibility check
14"""
15import logging
16from flask import Blueprint, request, jsonify, g
17from .models import get_db
19logger = logging.getLogger('hevolve_social')
21regional_host_bp = Blueprint('regional_host', __name__,
22 url_prefix='/api/social/regional-host')
25def _get_authenticated_user_id():
26 """Extract authenticated user_id from session/JWT — NOT from request body.
28 Falls back to g.user (set by require_auth decorator in social API).
29 """
30 # Check Flask g for auth context (set by @require_auth or before_request)
31 if hasattr(g, 'user') and g.user:
32 return getattr(g.user, 'id', None) or str(g.user)
34 # Check Authorization header for JWT token
35 auth_header = request.headers.get('Authorization', '')
36 if auth_header.startswith('Bearer '):
37 try:
38 from .auth_service import AuthService
39 token = auth_header.split(' ', 1)[1]
40 user_id = AuthService.verify_token(token)
41 return user_id
42 except Exception:
43 pass
45 return None
48def _require_admin(db, user_id):
49 """Check if user is admin (steward). user_id must come from auth, not body."""
50 if not user_id:
51 return False
52 from .models import User
53 user = db.query(User).get(user_id)
54 return user and getattr(user, 'is_admin', False)
57@regional_host_bp.route('/request', methods=['POST'])
58def request_regional_host():
59 """User requests to become a regional host."""
60 db = get_db()
61 try:
62 data = request.get_json(force=True)
63 # Auth: prefer authenticated user_id, fall back to body for non-auth setups
64 user_id = _get_authenticated_user_id() or data.get('user_id', '')
65 if not user_id:
66 return jsonify({'error': 'Authentication required'}), 401
68 from .regional_host_service import RegionalHostService
69 result = RegionalHostService.request_regional_host(
70 db,
71 user_id=str(user_id),
72 compute_info=data.get('compute_info', {}),
73 node_id=data.get('node_id', ''),
74 public_key_hex=data.get('public_key_hex', ''),
75 github_username=data.get('github_username', ''),
76 )
77 db.commit()
78 return jsonify(result), 200
79 except Exception as e:
80 db.rollback()
81 logger.error(f"Regional host request error: {e}")
82 return jsonify({'error': 'Internal server error'}), 500
83 finally:
84 db.close()
87@regional_host_bp.route('/requests', methods=['GET'])
88def list_pending_requests():
89 """Steward lists pending regional host requests."""
90 db = get_db()
91 try:
92 user_id = _get_authenticated_user_id()
93 if not _require_admin(db, user_id):
94 return jsonify({'error': 'Admin access required'}), 403
96 from .regional_host_service import RegionalHostService
97 results = RegionalHostService.list_pending_requests(db)
98 return jsonify({'requests': results}), 200
99 except Exception as e:
100 logger.error(f"List requests error: {e}")
101 return jsonify({'error': 'Internal server error'}), 500
102 finally:
103 db.close()
106@regional_host_bp.route('/approve', methods=['POST'])
107def approve_request():
108 """Steward approves a regional host request."""
109 db = get_db()
110 try:
111 data = request.get_json(force=True)
112 user_id = _get_authenticated_user_id()
113 if not _require_admin(db, user_id):
114 return jsonify({'error': 'Admin access required'}), 403
116 request_id = data.get('request_id', '')
117 region_name = data.get('region_name', '')
118 if not request_id or not region_name:
119 return jsonify({
120 'error': 'request_id and region_name required'}), 400
122 from .regional_host_service import RegionalHostService
123 result = RegionalHostService.approve_request(
124 db,
125 request_id=request_id,
126 steward_node_id=data.get('steward_node_id', user_id),
127 region_name=region_name,
128 )
129 db.commit()
130 return jsonify(result), 200
131 except Exception as e:
132 db.rollback()
133 logger.error(f"Approve request error: {e}")
134 return jsonify({'error': 'Internal server error'}), 500
135 finally:
136 db.close()
139@regional_host_bp.route('/reject', methods=['POST'])
140def reject_request():
141 """Steward rejects a regional host request."""
142 db = get_db()
143 try:
144 data = request.get_json(force=True)
145 user_id = _get_authenticated_user_id()
146 if not _require_admin(db, user_id):
147 return jsonify({'error': 'Admin access required'}), 403
149 request_id = data.get('request_id', '')
150 if not request_id:
151 return jsonify({'error': 'request_id required'}), 400
153 from .regional_host_service import RegionalHostService
154 result = RegionalHostService.reject_request(
155 db,
156 request_id=request_id,
157 reason=data.get('reason', ''),
158 )
159 db.commit()
160 return jsonify(result), 200
161 except Exception as e:
162 db.rollback()
163 logger.error(f"Reject request error: {e}")
164 return jsonify({'error': 'Internal server error'}), 500
165 finally:
166 db.close()
169@regional_host_bp.route('/revoke', methods=['POST'])
170def revoke_request():
171 """Steward revokes a regional host."""
172 db = get_db()
173 try:
174 data = request.get_json(force=True)
175 user_id = _get_authenticated_user_id()
176 if not _require_admin(db, user_id):
177 return jsonify({'error': 'Admin access required'}), 403
179 request_id = data.get('request_id', '')
180 if not request_id:
181 return jsonify({'error': 'request_id required'}), 400
183 from .regional_host_service import RegionalHostService
184 result = RegionalHostService.revoke_regional_host(
185 db, request_id=request_id)
186 db.commit()
187 return jsonify(result), 200
188 except Exception as e:
189 db.rollback()
190 logger.error(f"Revoke request error: {e}")
191 return jsonify({'error': 'Internal server error'}), 500
192 finally:
193 db.close()
196@regional_host_bp.route('/status', methods=['GET'])
197def check_status():
198 """User checks their regional host request status."""
199 db = get_db()
200 try:
201 # Auth: only allow checking own status (prevent IDOR)
202 user_id = _get_authenticated_user_id() or request.args.get('user_id', '')
203 if not user_id:
204 return jsonify({'error': 'Authentication required'}), 401
206 from .regional_host_service import RegionalHostService
207 result = RegionalHostService.get_request_status(db, str(user_id))
208 if result is None:
209 return jsonify({'status': 'no_request'}), 200
210 return jsonify(result), 200
211 except Exception as e:
212 logger.error(f"Status check error: {e}")
213 return jsonify({'error': 'Internal server error'}), 500
214 finally:
215 db.close()
218@regional_host_bp.route('/capacity', methods=['GET'])
219def region_capacity():
220 """Get capacity metrics for a specific region or all regions."""
221 db = get_db()
222 try:
223 region_name = request.args.get('region', '')
224 from .regional_host_service import RegionalHostService
226 if region_name:
227 result = RegionalHostService.get_region_capacity(db, region_name)
228 else:
229 result = {
230 'regions': RegionalHostService.get_all_region_capacities(db),
231 }
232 return jsonify(result), 200
233 except Exception as e:
234 logger.error(f"Capacity check error: {e}")
235 return jsonify({'error': 'Internal server error'}), 500
236 finally:
237 db.close()
240@regional_host_bp.route('/rebalance', methods=['GET'])
241def suggest_rebalance():
242 """Get elastic rebalancing suggestions (steward dashboard)."""
243 db = get_db()
244 try:
245 user_id = _get_authenticated_user_id()
246 if not _require_admin(db, user_id):
247 return jsonify({'error': 'Admin access required'}), 403
249 from .regional_host_service import RegionalHostService
250 result = RegionalHostService.suggest_rebalance(db)
251 return jsonify(result), 200
252 except Exception as e:
253 logger.error(f"Rebalance suggestion error: {e}")
254 return jsonify({'error': 'Internal server error'}), 500
255 finally:
256 db.close()
259@regional_host_bp.route('/scaling', methods=['GET'])
260def scaling_check():
261 """Check if any regions need horizontal scaling."""
262 db = get_db()
263 try:
264 user_id = _get_authenticated_user_id()
265 if not _require_admin(db, user_id):
266 return jsonify({'error': 'Admin access required'}), 403
268 from .regional_host_service import RegionalHostService
269 result = RegionalHostService.check_scaling_needed(db)
270 return jsonify(result), 200
271 except Exception as e:
272 logger.error(f"Scaling check error: {e}")
273 return jsonify({'error': 'Internal server error'}), 500
274 finally:
275 db.close()
278@regional_host_bp.route('/eligibility', methods=['GET'])
279def check_eligibility():
280 """Check if user meets minimum requirements to request regional host.
282 Returns compute tier, trust score, and minimum requirements so the
283 frontend can show requirements and disable the button if not met.
284 """
285 db = get_db()
286 try:
287 user_id = _get_authenticated_user_id() or request.args.get('user_id', '')
288 if not user_id:
289 return jsonify({'error': 'Authentication required'}), 401
291 # Get compute tier (server-side detection)
292 compute_tier = 'UNKNOWN'
293 compute_info = {}
294 try:
295 from security.system_requirements import detect_hardware, classify_tier
296 hw = detect_hardware()
297 compute_tier = classify_tier(hw)
298 compute_info = {
299 'cpu_cores': hw.get('cpu_cores', 0),
300 'ram_gb': round(hw.get('ram_bytes', 0) / (1024**3), 1),
301 'gpu_count': hw.get('gpu_count', 0),
302 'gpu_name': hw.get('gpu_name', ''),
303 }
304 except Exception:
305 pass
307 # Get trust score
308 trust_score = 0.0
309 try:
310 from .rating_service import RatingService
311 ts = RatingService.get_trust_score(db, str(user_id))
312 if ts:
313 trust_score = ts.get('composite_trust', 0.0)
314 except Exception:
315 pass
317 # Check existing request
318 existing_request = None
319 try:
320 from .regional_host_service import RegionalHostService
321 existing_request = RegionalHostService.get_request_status(db, str(user_id))
322 except Exception:
323 pass
325 # Tier ranking
326 tier_rank = {'OBSERVER': 0, 'BASIC': 1, 'STANDARD': 2, 'ADVANCED': 3, 'COMPUTE_HOST': 4}
327 current_rank = tier_rank.get(compute_tier, -1)
328 min_rank = tier_rank.get('STANDARD', 2)
330 meets_compute = current_rank >= min_rank
331 meets_trust = trust_score >= 2.5
332 eligible = meets_compute and meets_trust
334 return jsonify({
335 'eligible': eligible,
336 'compute_tier': compute_tier,
337 'compute_info': compute_info,
338 'trust_score': round(trust_score, 2),
339 'requirements': {
340 'min_compute_tier': 'STANDARD',
341 'min_trust_score': 2.5,
342 'compute_tiers_ranked': ['OBSERVER', 'BASIC', 'STANDARD', 'ADVANCED', 'COMPUTE_HOST'],
343 'compute_description': {
344 'STANDARD': '4+ CPU cores, 8+ GB RAM',
345 'ADVANCED': '8+ CPU cores, 16+ GB RAM, GPU recommended',
346 'COMPUTE_HOST': '16+ cores, 32+ GB RAM, dedicated GPU',
347 },
348 },
349 'meets_compute': meets_compute,
350 'meets_trust': meets_trust,
351 'existing_request': existing_request,
352 }), 200
353 except Exception as e:
354 logger.error(f"Eligibility check error: {e}")
355 return jsonify({'error': 'Internal server error'}), 500
356 finally:
357 db.close()