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

1""" 

2Regional Host API - Blueprint for regional host request + approval. 

3 

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 

18 

19logger = logging.getLogger('hevolve_social') 

20 

21regional_host_bp = Blueprint('regional_host', __name__, 

22 url_prefix='/api/social/regional-host') 

23 

24 

25def _get_authenticated_user_id(): 

26 """Extract authenticated user_id from session/JWT — NOT from request body. 

27 

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) 

33 

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 

44 

45 return None 

46 

47 

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) 

55 

56 

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 

67 

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() 

85 

86 

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 

95 

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() 

104 

105 

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 

115 

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 

121 

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() 

137 

138 

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 

148 

149 request_id = data.get('request_id', '') 

150 if not request_id: 

151 return jsonify({'error': 'request_id required'}), 400 

152 

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() 

167 

168 

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 

178 

179 request_id = data.get('request_id', '') 

180 if not request_id: 

181 return jsonify({'error': 'request_id required'}), 400 

182 

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() 

194 

195 

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 

205 

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() 

216 

217 

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 

225 

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() 

238 

239 

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 

248 

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() 

257 

258 

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 

267 

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() 

276 

277 

278@regional_host_bp.route('/eligibility', methods=['GET']) 

279def check_eligibility(): 

280 """Check if user meets minimum requirements to request regional host. 

281 

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 

290 

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 

306 

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 

316 

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 

324 

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) 

329 

330 meets_compute = current_rank >= min_rank 

331 meets_trust = trust_score >= 2.5 

332 eligible = meets_compute and meets_trust 

333 

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()