Phase 3.4: mitra_app self-managed auth cutover

Rips firebase_auth; phone OTP flow now talks directly to the new
backend endpoints, JWT access token lives in memory, refresh token
persists via flutter_secure_storage. WebSocket handshakes read the
access token from AuthBridge instead of Firebase.

Smoke-tested end-to-end against the backend via curl:
- otp/request → read stub code from backend log → otp/verify
- /api/mitra/auth/me + /api/shared/auth/refresh rotation
- logout → post-logout refresh correctly fails REFRESH_INVALID
- ACCOUNT_INACTIVE (403) + WRONG_FLOW (400) error paths verified
- Debug APK links cleanly

- pubspec: drop firebase_auth, add flutter_secure_storage
- core/auth/auth_bridge.dart: shared mutable state (access token +
  refresh callback + in-flight de-dup) as keepAlive provider
- core/auth/token_storage.dart: flutter_secure_storage wrapper
- core/auth/auth_notifier.dart: bootstrap → refresh; requestOtp +
  verifyOtp via /api/mitra/auth/*; logout; granular OTP error codes
- core/api/api_client.dart: Bearer from bridge + postRaw(skipAuth) for
  auth endpoints + single-retry 401 refresh
- core/chat/*_notifier.dart: WS auth frame reads bridge.accessToken
- features/auth/screens/otp_screen.dart: verificationId → otpRequestId
- mitra_app/CLAUDE.md: Auth section rewritten (was stale on Firebase)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 15:58:25 +08:00
parent 4a796277b8
commit 2b61c79a86
17 changed files with 496 additions and 140 deletions

View File

@@ -15,14 +15,14 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
final List<TextEditingController> _controllers =
List.generate(6, (_) => TextEditingController());
final List<FocusNode> _focusNodes = List.generate(6, (_) => FocusNode());
String? _verificationId;
String? _otpRequestId;
@override
void initState() {
super.initState();
final data = ref.read(mitraAuthProvider).valueOrNull;
if (data is MitraAuthOtpSentData) {
_verificationId = data.verificationId;
_otpRequestId = data.otpRequestId;
}
}
@@ -60,8 +60,8 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
void _submit() {
final otp = _otp;
if (otp.length != 6 || _verificationId == null) return;
ref.read(mitraAuthProvider.notifier).verifyOtp(_verificationId!, otp);
if (otp.length != 6 || _otpRequestId == null) return;
ref.read(mitraAuthProvider.notifier).verifyOtp(_otpRequestId!, otp);
}
@override
@@ -69,10 +69,10 @@ class _OtpScreenState extends ConsumerState<OtpScreen> {
final authState = ref.watch(mitraAuthProvider);
final isLoading = authState is AsyncLoading;
// Update verification ID if state changes
// Update OTP request id if state changes (e.g. resend)
final data = authState.valueOrNull;
if (data is MitraAuthOtpSentData) {
_verificationId = data.verificationId;
_otpRequestId = data.otpRequestId;
}
ref.listen(mitraAuthProvider, (prev, next) {