Files
mobile-app/lib/core/services/twitter_auth_service.dart

117 lines
3.9 KiB
Dart

import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:real_estate_mobile/config/app_config.dart';
/// Twitter OAuth 2.0 PKCE flow for mobile.
class TwitterAuthService {
static const _clientId = 'eWEzaDJ0al9lX1diOGY0ejRYeXM6MTpjaQ';
static const _clientSecret = 'HadGMSRKKswfXOajbmggrnmLAn9cxJsl8GtFxGHqjX-CJ0A2Tu';
static const _scope = 'tweet.read users.read offline.access';
/// Derive redirect URI from API base URL (matches next-auth callback)
static String get _redirectUri {
final apiUrl = AppConfig.apiBaseUrl; // e.g. https://dev.re-quest.com/api/v1
final base = apiUrl.replaceAll(RegExp(r'/api/v1/?$'), '');
return '$base/api/auth/callback/twitter';
}
late final String _codeVerifier;
late final String _codeChallenge;
late final String _stateParam;
TwitterAuthService() {
_codeVerifier = _generateCodeVerifier();
_codeChallenge = _generateCodeChallenge(_codeVerifier);
_stateParam = _generateRandomString(32);
}
/// Build the Twitter authorization URL to open in a webview.
String get authorizationUrl {
final params = {
'response_type': 'code',
'client_id': _clientId,
'redirect_uri': _redirectUri,
'scope': _scope,
'state': _stateParam,
'code_challenge': _codeChallenge,
'code_challenge_method': 'S256',
};
final query = params.entries
.map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}')
.join('&');
return 'https://twitter.com/i/oauth2/authorize?$query';
}
/// Check if a URL is the redirect callback and extract the auth code.
String? extractCodeFromUrl(String url) {
if (!url.startsWith(_redirectUri)) return null;
final uri = Uri.parse(url);
final state = uri.queryParameters['state'];
if (state != _stateParam) return null;
return uri.queryParameters['code'];
}
/// Exchange the auth code for tokens and fetch user info.
Future<Map<String, dynamic>?> exchangeCodeForUser(String code) async {
final dio = Dio();
// Exchange code for access token (confidential client — Basic auth)
final credentials = base64Encode(utf8.encode('$_clientId:$_clientSecret'));
final tokenBody =
'code=${Uri.encodeComponent(code)}'
'&grant_type=authorization_code'
'&redirect_uri=${Uri.encodeComponent(_redirectUri)}'
'&code_verifier=${Uri.encodeComponent(_codeVerifier)}';
final tokenResponse = await dio.post(
'https://api.twitter.com/2/oauth2/token',
options: Options(
contentType: 'application/x-www-form-urlencoded',
headers: {
'Authorization': 'Basic $credentials',
},
),
data: tokenBody,
);
final accessToken = tokenResponse.data['access_token'] as String?;
if (accessToken == null) return null;
// Fetch user info
final userResponse = await dio.get(
'https://api.twitter.com/2/users/me',
queryParameters: {'user.fields': 'id,name,username,profile_image_url'},
options: Options(
headers: {'Authorization': 'Bearer $accessToken'},
),
);
final userData = userResponse.data['data'] as Map<String, dynamic>?;
if (userData == null) return null;
return {
'id': userData['id'],
'name': userData['name'],
'username': userData['username'],
'avatar': userData['profile_image_url'],
};
}
String _generateRandomString(int length) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
final random = Random.secure();
return List.generate(length, (_) => chars[random.nextInt(chars.length)]).join();
}
String _generateCodeVerifier() => _generateRandomString(64);
String _generateCodeChallenge(String verifier) {
final bytes = utf8.encode(verifier);
final digest = sha256.convert(bytes);
return base64Url.encode(digest.bytes).replaceAll('=', '');
}
}