초기 커밋
This commit is contained in:
26
lib/core/network/auth_interceptor.dart
Normal file
26
lib/core/network/auth_interceptor.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../constants/app_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
class AuthInterceptor extends Interceptor {
|
||||
AuthInterceptor({required this.ref});
|
||||
|
||||
final Ref ref;
|
||||
|
||||
@override
|
||||
Future<void> onRequest(
|
||||
RequestOptions options,
|
||||
RequestInterceptorHandler handler,
|
||||
) async {
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
final token = await secureStorage.read(key: AppConstants.accessTokenKey);
|
||||
|
||||
if (token != null) {
|
||||
options.headers['Authorization'] = 'Bearer $token';
|
||||
}
|
||||
|
||||
handler.next(options);
|
||||
}
|
||||
}
|
||||
44
lib/core/network/dio_client.dart
Normal file
44
lib/core/network/dio_client.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:talker_dio_logger/talker_dio_logger.dart';
|
||||
|
||||
import '../constants/api_constants.dart';
|
||||
import '../logging/app_logger.dart';
|
||||
import 'auth_interceptor.dart';
|
||||
import 'token_refresh_interceptor.dart';
|
||||
|
||||
part 'dio_client.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Dio dio(Ref ref) {
|
||||
final dio = Dio(
|
||||
BaseOptions(
|
||||
baseUrl: ApiConstants.baseUrl,
|
||||
connectTimeout: ApiConstants.connectTimeout,
|
||||
receiveTimeout: ApiConstants.receiveTimeout,
|
||||
sendTimeout: ApiConstants.sendTimeout,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
final talker = ref.read(appLoggerProvider);
|
||||
|
||||
dio.interceptors.addAll([
|
||||
AuthInterceptor(ref: ref),
|
||||
TokenRefreshInterceptor(ref: ref, dio: dio),
|
||||
TalkerDioLogger(
|
||||
talker: talker,
|
||||
settings: const TalkerDioLoggerSettings(
|
||||
printRequestHeaders: true,
|
||||
printResponseHeaders: false,
|
||||
printResponseData: true,
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
return dio;
|
||||
}
|
||||
109
lib/core/network/socket_manager.dart
Normal file
109
lib/core/network/socket_manager.dart
Normal file
@@ -0,0 +1,109 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:talker/talker.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../constants/api_constants.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../logging/app_logger.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
part 'socket_manager.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
SocketManager socketManager(Ref ref) {
|
||||
final talker = ref.read(appLoggerProvider);
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
final manager = SocketManager(talker: talker, secureStorage: secureStorage);
|
||||
ref.onDispose(manager.dispose);
|
||||
return manager;
|
||||
}
|
||||
|
||||
class SocketManager {
|
||||
SocketManager({
|
||||
required this.talker,
|
||||
required this.secureStorage,
|
||||
});
|
||||
|
||||
final Talker talker;
|
||||
final SecureStorage secureStorage;
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
Timer? _reconnectTimer;
|
||||
final _messageController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
bool _isConnected = false;
|
||||
int _reconnectAttempts = 0;
|
||||
static const int _maxReconnectAttempts = 5;
|
||||
|
||||
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<void> connect({String? path}) async {
|
||||
try {
|
||||
final token = await secureStorage.read(key: AppConstants.accessTokenKey);
|
||||
final wsUrl = '${ApiConstants.wsUrl}${path ?? ''}';
|
||||
final uri = Uri.parse(
|
||||
token != null ? '$wsUrl?token=$token' : wsUrl,
|
||||
);
|
||||
|
||||
_channel = WebSocketChannel.connect(uri);
|
||||
await _channel!.ready;
|
||||
_isConnected = true;
|
||||
_reconnectAttempts = 0;
|
||||
|
||||
talker.info('WebSocket connected: $wsUrl');
|
||||
|
||||
_channel!.stream.listen(
|
||||
(data) {
|
||||
try {
|
||||
final decoded = jsonDecode(data as String) as Map<String, dynamic>;
|
||||
_messageController.add(decoded);
|
||||
} catch (e) {
|
||||
talker.error('WebSocket message parse error', e);
|
||||
}
|
||||
},
|
||||
onError: (Object error) {
|
||||
talker.error('WebSocket error', error);
|
||||
_handleDisconnect();
|
||||
},
|
||||
onDone: () {
|
||||
talker.warning('WebSocket disconnected');
|
||||
_handleDisconnect();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
talker.error('WebSocket connection failed', e);
|
||||
_handleDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDisconnect() {
|
||||
_isConnected = false;
|
||||
if (_reconnectAttempts < _maxReconnectAttempts) {
|
||||
final delay = Duration(seconds: _reconnectAttempts * 2 + 1);
|
||||
_reconnectTimer = Timer(delay, () {
|
||||
_reconnectAttempts++;
|
||||
talker.info('WebSocket reconnect attempt $_reconnectAttempts');
|
||||
connect();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void send(Map<String, dynamic> data) {
|
||||
if (_isConnected && _channel != null) {
|
||||
_channel!.sink.add(jsonEncode(data));
|
||||
} else {
|
||||
talker.warning('WebSocket not connected, message not sent');
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_reconnectTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_messageController.close();
|
||||
_isConnected = false;
|
||||
}
|
||||
}
|
||||
77
lib/core/network/token_refresh_interceptor.dart
Normal file
77
lib/core/network/token_refresh_interceptor.dart
Normal file
@@ -0,0 +1,77 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../constants/api_constants.dart';
|
||||
import '../constants/app_constants.dart';
|
||||
import '../storage/secure_storage.dart';
|
||||
|
||||
class TokenRefreshInterceptor extends Interceptor {
|
||||
TokenRefreshInterceptor({
|
||||
required this.ref,
|
||||
required this.dio,
|
||||
});
|
||||
|
||||
final Ref ref;
|
||||
final Dio dio;
|
||||
bool _isRefreshing = false;
|
||||
|
||||
@override
|
||||
Future<void> onError(
|
||||
DioException err,
|
||||
ErrorInterceptorHandler handler,
|
||||
) async {
|
||||
if (err.response?.statusCode == 401 && !_isRefreshing) {
|
||||
_isRefreshing = true;
|
||||
try {
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
final refreshToken = await secureStorage.read(
|
||||
key: AppConstants.refreshTokenKey,
|
||||
);
|
||||
|
||||
if (refreshToken == null) {
|
||||
_isRefreshing = false;
|
||||
return handler.next(err);
|
||||
}
|
||||
|
||||
// Request new tokens
|
||||
final refreshDio = Dio(
|
||||
BaseOptions(baseUrl: ApiConstants.baseUrl),
|
||||
);
|
||||
final response = await refreshDio.post(
|
||||
ApiConstants.refreshToken,
|
||||
data: {'refresh_token': refreshToken},
|
||||
);
|
||||
|
||||
final newAccessToken = response.data['access_token'] as String;
|
||||
final newRefreshToken = response.data['refresh_token'] as String;
|
||||
|
||||
// Save new tokens
|
||||
await secureStorage.write(
|
||||
key: AppConstants.accessTokenKey,
|
||||
value: newAccessToken,
|
||||
);
|
||||
await secureStorage.write(
|
||||
key: AppConstants.refreshTokenKey,
|
||||
value: newRefreshToken,
|
||||
);
|
||||
|
||||
// Retry original request
|
||||
final options = err.requestOptions;
|
||||
options.headers['Authorization'] = 'Bearer $newAccessToken';
|
||||
|
||||
final retryResponse = await dio.fetch(options);
|
||||
_isRefreshing = false;
|
||||
return handler.resolve(retryResponse);
|
||||
} on DioException {
|
||||
_isRefreshing = false;
|
||||
// Token refresh failed, clear tokens
|
||||
final secureStorage = ref.read(secureStorageProvider);
|
||||
await secureStorage.delete(key: AppConstants.accessTokenKey);
|
||||
await secureStorage.delete(key: AppConstants.refreshTokenKey);
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return handler.next(err);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user