import 'dart:io'; import 'dart:convert'; import 'dart:math'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import 'package:island/pods/network.dart'; import 'package:island/talker.dart'; class WebAuthServer { final Ref _ref; HttpServer? _server; String? _challenge; DateTime? _challengeTimestamp; final _challengeTtl = const Duration(seconds: 30); WebAuthServer(this._ref); Future start() async { if (_server != null) { talker.warning('Web auth server already running.'); return _server!.port; } final port = await _findUnusedPort(40000, 41000); _server = await HttpServer.bind(InternetAddress.loopbackIPv4, port); talker.info('Web auth server started on http://127.0.0.1:$port'); _server!.listen(_handleRequest); return port; } void stop() { _server?.close(force: true); _server = null; talker.info('Web auth server stopped.'); } Future _findUnusedPort(int start, int end) async { for (var port = start; port <= end; port++) { try { var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, port); await socket.close(); return port; } catch (e) { // Port is in use, try next } } throw Exception('No unused port found in range $start-$end'); } String _generateChallenge() { final random = Random.secure(); final values = List.generate(32, (i) => random.nextInt(256)); return base64Url.encode(values); } void _addCorsHeaders(HttpResponse response) { const webUrl = 'https://app.solian.fr'; response.headers.add('Access-Control-Allow-Origin', webUrl); response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); response.headers.add('Access-Control-Allow-Headers', '*'); } Future _handleRequest(HttpRequest request) async { try { _addCorsHeaders(request.response); if (request.method == 'OPTIONS') { request.response.statusCode = HttpStatus.noContent; await request.response.close(); return; } talker.info('Web auth request: ${request.method} ${request.uri.path}'); if (request.method == 'GET' && request.uri.path == '/alive') { await _handleAlive(request); } else if (request.method == 'POST' && request.uri.path == '/exchange') { await _handleExchange(request); } else { request.response.statusCode = HttpStatus.notFound; request.response.write(jsonEncode({'error': 'Not Found'})); await request.response.close(); } } catch (e, st) { talker.handle(e, st, 'Error handling web auth request'); try { request.response.statusCode = HttpStatus.internalServerError; request.response.write(jsonEncode({'error': 'Internal Server Error'})); await request.response.close(); } catch (e2) { talker.error('Failed to send error response: $e2'); } } } Future _handleAlive(HttpRequest request) async { _challenge = _generateChallenge(); _challengeTimestamp = DateTime.now(); final response = { 'status': 'ok', 'challenge': _challenge, }; request.response.statusCode = HttpStatus.ok; request.response.headers.contentType = ContentType.json; request.response.write(jsonEncode(response)); await request.response.close(); } Future _handleExchange(HttpRequest request) async { if (_challenge == null || _challengeTimestamp == null || DateTime.now().difference(_challengeTimestamp!) > _challengeTtl) { request.response.statusCode = HttpStatus.badRequest; request.response.write(jsonEncode({ 'error': 'Invalid or expired challenge. Please call /alive first.' })); await request.response.close(); return; } final requestBody = await utf8.decodeStream(request); final Map data; try { data = jsonDecode(requestBody); } catch (e) { request.response.statusCode = HttpStatus.badRequest; request.response.write(jsonEncode({'error': 'Invalid JSON body'})); await request.response.close(); return; } final String? signedChallenge = data['signedChallenge']; final Map? deviceInfo = data['deviceInfo']; if (signedChallenge == null) { request.response.statusCode = HttpStatus.badRequest; request.response.write(jsonEncode({'error': 'Missing signedChallenge'})); await request.response.close(); return; } final currentChallenge = _challenge!; _challenge = null; _challengeTimestamp = null; try { final dio = _ref.read(apiClientProvider); final response = await dio.post( '/pass/auth/login/session', data: { 'signedChallenge': signedChallenge, 'challenge': currentChallenge, ...?deviceInfo, }, ); if (response.statusCode == 200 && response.data != null) { final webToken = response.data['token']; request.response.statusCode = HttpStatus.ok; request.response.write(jsonEncode({'token': webToken})); } else { throw Exception( 'Backend exchange failed with status ${response.statusCode}'); } } on DioException catch (e) { talker.error('Backend exchange failed: ${e.response?.data}'); request.response.statusCode = e.response?.statusCode ?? HttpStatus.internalServerError; request.response.write( jsonEncode(e.response?.data ?? {'error': 'Backend communication failed'})); } catch (e, st) { talker.handle(e, st, 'Error during backend exchange'); request.response.statusCode = HttpStatus.internalServerError; request.response .write(jsonEncode({'error': 'An unexpected error occurred'})); } finally { await request.response.close(); } } }