web: migrate health notifications to js_interop; add browser hook
- Replace dart:js with package:js in health_check_service_web.dart\n- Implement showHealthCheckNotification in web/index.html\n- Pin js dependency to ^0.6.7 for flutter_secure_storage_web compatibility auth: harden AuthInterceptor + tests - Allow overrideAuthRepository injection for testing\n- Normalize imports to package: paths\n- Add unit test covering token attach, 401→refresh→retry, and failure path\n- Add integration test skeleton gated by env vars ui/data: map User.companyName to list column - Add companyName to domain User\n- Map UserDto.company?.name\n- Render companyName in user_list cleanup: remove legacy equipment table + unused code; minor warnings - Remove _buildFlexibleTable and unused helpers\n- Remove unused zipcode details and cache retry constant\n- Fix null-aware and non-null assertions\n- Address child-last warnings in administrator dialog docs: update AGENTS.md session context
This commit is contained in:
@@ -1,24 +1,28 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import '../../../../core/constants/api_endpoints.dart';
|
||||
import '../../../../services/auth_service.dart';
|
||||
import '../../../../core/config/environment.dart';
|
||||
import 'package:superport/core/constants/api_endpoints.dart';
|
||||
import 'package:superport/domain/repositories/auth_repository.dart';
|
||||
import 'package:superport/data/models/auth/refresh_token_request.dart';
|
||||
import 'package:superport/core/config/environment.dart';
|
||||
import 'package:superport/core/navigation/app_navigator.dart';
|
||||
|
||||
/// 인증 인터셉터
|
||||
class AuthInterceptor extends Interceptor {
|
||||
AuthService? _authService;
|
||||
AuthRepository? _authRepository;
|
||||
final Dio dio;
|
||||
|
||||
AuthInterceptor(this.dio);
|
||||
AuthInterceptor(this.dio, {AuthRepository? overrideAuthRepository}) {
|
||||
_authRepository = overrideAuthRepository;
|
||||
}
|
||||
|
||||
AuthService? get authService {
|
||||
AuthRepository? get authRepository {
|
||||
try {
|
||||
_authService ??= GetIt.instance<AuthService>();
|
||||
return _authService;
|
||||
_authRepository ??= GetIt.instance<AuthRepository>();
|
||||
return _authRepository;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint('Failed to get AuthService in AuthInterceptor: $e');
|
||||
debugPrint('Failed to get AuthRepository in AuthInterceptor: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -43,21 +47,22 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
|
||||
// 저장된 액세스 토큰 가져오기
|
||||
final service = authService;
|
||||
final repo = authRepository;
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] AuthService available: ${service != null}');
|
||||
debugPrint('[AuthInterceptor] AuthRepository available: ${repo != null}');
|
||||
}
|
||||
|
||||
if (service != null) {
|
||||
final accessToken = await service.getAccessToken();
|
||||
if (repo != null) {
|
||||
final tokenEither = await repo.getStoredAccessToken();
|
||||
final accessToken = tokenEither.fold((_) => null, (t) => t);
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Access token retrieved: ${accessToken != null ? 'Yes (${accessToken.substring(0, 10)}...)' : 'No'}');
|
||||
debugPrint('[AuthInterceptor] Access token retrieved: ${accessToken != null ? 'Yes' : 'No'}');
|
||||
}
|
||||
|
||||
if (accessToken != null) {
|
||||
options.headers['Authorization'] = 'Bearer $accessToken';
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Authorization header set: Bearer ${accessToken.substring(0, 10)}...');
|
||||
debugPrint('[AuthInterceptor] Authorization header set');
|
||||
}
|
||||
} else {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
@@ -66,7 +71,7 @@ class AuthInterceptor extends Interceptor {
|
||||
}
|
||||
} else {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] ERROR: AuthService not available from GetIt');
|
||||
debugPrint('[AuthInterceptor] ERROR: AuthRepository not available from GetIt');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,33 +101,25 @@ class AuthInterceptor extends Interceptor {
|
||||
return;
|
||||
}
|
||||
|
||||
final service = authService;
|
||||
if (service != null) {
|
||||
final repo = authRepository;
|
||||
if (repo != null) {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Attempting token refresh...');
|
||||
}
|
||||
// 토큰 갱신 시도
|
||||
final refreshResult = await service.refreshToken();
|
||||
final refreshTokenEither = await repo.getStoredRefreshToken();
|
||||
final refreshToken = refreshTokenEither.fold((_) => null, (t) => t);
|
||||
final refreshResult = refreshToken == null
|
||||
? null
|
||||
: await repo.refreshToken(RefreshTokenRequest(refreshToken: refreshToken));
|
||||
|
||||
final refreshSuccess = refreshResult.fold(
|
||||
(failure) {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Token refresh failed: ${failure.message}');
|
||||
}
|
||||
return false;
|
||||
},
|
||||
(tokenResponse) {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Token refresh successful');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
final refreshSuccess = refreshResult != null && refreshResult.isRight();
|
||||
|
||||
if (refreshSuccess) {
|
||||
// 새로운 토큰으로 원래 요청 재시도
|
||||
try {
|
||||
final newAccessToken = await service.getAccessToken();
|
||||
final newAccessTokenEither = await repo.getStoredAccessToken();
|
||||
final newAccessToken = newAccessTokenEither.fold((_) => null, (t) => t);
|
||||
|
||||
if (newAccessToken != null) {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
@@ -149,8 +146,9 @@ class AuthInterceptor extends Interceptor {
|
||||
if (Environment.enableLogging && kDebugMode) {
|
||||
debugPrint('[AuthInterceptor] Clearing session due to auth failure');
|
||||
}
|
||||
await service.clearSession();
|
||||
// TODO: Navigate to login screen
|
||||
await repo.clearLocalSession();
|
||||
// 로그인 화면으로 이동 (모든 스택 제거)
|
||||
navigateToLoginClearingStack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,4 +162,4 @@ class AuthInterceptor extends Interceptor {
|
||||
path.contains(ApiEndpoints.refresh) ||
|
||||
path.contains(ApiEndpoints.logout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class UserDto with _$UserDto {
|
||||
name: name,
|
||||
email: email,
|
||||
phone: phone,
|
||||
companyName: company?.name,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,4 +121,3 @@ class CheckUsernameResponse with _$CheckUsernameResponse {
|
||||
factory CheckUsernameResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$CheckUsernameResponseFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,42 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, String?>> getStoredRefreshToken() async {
|
||||
try {
|
||||
final token = await _getRefreshToken();
|
||||
return Right(token);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '리프레시 토큰 조회 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, String?>> getStoredAccessToken() async {
|
||||
try {
|
||||
final token = await _getAccessToken();
|
||||
return Right(token);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '액세스 토큰 조회 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> clearLocalSession() async {
|
||||
try {
|
||||
await _clearLocalData();
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
return Left(ServerFailure(
|
||||
message: '로컬 세션 정리 중 오류가 발생했습니다: ${e.toString()}',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Private 헬퍼 메서드들
|
||||
|
||||
/// 액세스 토큰과 리프레시 토큰을 로컬에 저장
|
||||
|
||||
Reference in New Issue
Block a user