Fix auth errors, CORS, control center login, and stale session handling
- Mitra auth: parse DioException response for proper error messages (ACCOUNT_NOT_FOUND, ACCOUNT_INACTIVE) instead of generic "OTP invalid" - Backend: add CORS to internal app (port 3001) for control center - Control center: fix login race condition (wait for AuthContext verify before navigating), fix MitraActivityPage fetching paginated data - Stale session goodbye: both apps detect SESSION_NOT_ACTIVE/409 and move to complete state instead of retrying endlessly Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import Fastify from 'fastify'
|
import Fastify from 'fastify'
|
||||||
|
import cors from '@fastify/cors'
|
||||||
import sensible from '@fastify/sensible'
|
import sensible from '@fastify/sensible'
|
||||||
import { mitraManagementRoutes } from './routes/internal/mitra.routes.js'
|
import { mitraManagementRoutes } from './routes/internal/mitra.routes.js'
|
||||||
import { ccUserRoutes } from './routes/internal/cc-user.routes.js'
|
import { ccUserRoutes } from './routes/internal/cc-user.routes.js'
|
||||||
@@ -12,6 +13,7 @@ import { errorHandler } from './plugins/error-handler.js'
|
|||||||
export const buildInternalApp = async () => {
|
export const buildInternalApp = async () => {
|
||||||
const app = Fastify({ logger: true })
|
const app = Fastify({ logger: true })
|
||||||
|
|
||||||
|
await app.register(cors, { origin: true })
|
||||||
await app.register(sensible)
|
await app.register(sensible)
|
||||||
app.setErrorHandler(errorHandler)
|
app.setErrorHandler(errorHandler)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import '../api/api_client_provider.dart';
|
import '../api/api_client_provider.dart';
|
||||||
@@ -67,6 +68,13 @@ class SessionClosure extends _$SessionClosure {
|
|||||||
'message': message,
|
'message': message,
|
||||||
});
|
});
|
||||||
state = const ClosureCompleteData();
|
state = const ClosureCompleteData();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final code = e.response?.data?['error']?['code'];
|
||||||
|
if (code == 'SESSION_NOT_ACTIVE' || e.response?.statusCode == 409) {
|
||||||
|
state = const ClosureCompleteData();
|
||||||
|
} else {
|
||||||
|
state = const ClosureErrorData('Gagal mengirim pesan penutup.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = const ClosureErrorData('Gagal mengirim pesan penutup.');
|
state = const ClosureErrorData('Gagal mengirim pesan penutup.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,10 +125,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.0"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -628,18 +628,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.17"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.0"
|
version: "0.11.1"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -961,10 +961,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.10"
|
version: "0.7.7"
|
||||||
timezone:
|
timezone:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
3093
control_center/package-lock.json
generated
Normal file
3093
control_center/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,27 @@
|
|||||||
import { useState } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useAuth } from '../../core/auth/AuthContext'
|
import { useAuth } from '../../core/auth/AuthContext'
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { login } = useAuth()
|
const { user, loading: authLoading, login } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) navigate('/', { replace: true })
|
||||||
|
}, [user, navigate])
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await login(email, password)
|
await login(email, password)
|
||||||
navigate('/')
|
|
||||||
} catch {
|
} catch {
|
||||||
setError('Email atau password salah.')
|
setError('Email atau password salah.')
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ const fetchLog = async ({ mitra_id, date_from, date_to, page, limit }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchMitras = async () => {
|
const fetchMitras = async () => {
|
||||||
const res = await apiClient.get('/internal/mitras')
|
const res = await apiClient.get('/internal/mitras?limit=100')
|
||||||
return res.data.data
|
return res.data.data.items
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseColor = (response) => {
|
const responseColor = (response) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -91,8 +92,10 @@ class MitraAuth extends _$MitraAuth {
|
|||||||
await _auth.signInWithCredential(credential);
|
await _auth.signInWithCredential(credential);
|
||||||
}
|
}
|
||||||
state = AsyncData(await _verifyAndReturn());
|
state = AsyncData(await _verifyAndReturn());
|
||||||
} catch (e) {
|
} on FirebaseAuthException {
|
||||||
state = AsyncError('OTP tidak valid. Coba lagi.', StackTrace.current);
|
state = AsyncError('OTP tidak valid. Coba lagi.', StackTrace.current);
|
||||||
|
} catch (e) {
|
||||||
|
state = AsyncError(e.toString().replaceFirst('Exception: ', ''), StackTrace.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,15 +108,18 @@ class MitraAuth extends _$MitraAuth {
|
|||||||
try {
|
try {
|
||||||
final response = await _apiClient.post('/api/mitra/auth/verify');
|
final response = await _apiClient.post('/api/mitra/auth/verify');
|
||||||
return MitraAuthAuthenticatedData(response['data'] as Map<String, dynamic>);
|
return MitraAuthAuthenticatedData(response['data'] as Map<String, dynamic>);
|
||||||
} on Exception catch (e) {
|
} on DioException catch (e) {
|
||||||
await _auth.signOut();
|
await _auth.signOut();
|
||||||
final msg = e.toString();
|
final code = e.response?.data?['error']?['code'] as String?;
|
||||||
if (msg.contains('ACCOUNT_NOT_FOUND')) {
|
if (code == 'ACCOUNT_NOT_FOUND' || e.response?.statusCode == 404) {
|
||||||
throw Exception('Akun tidak ditemukan. Hubungi administrator.');
|
throw Exception('Akun tidak ditemukan. Hubungi administrator.');
|
||||||
} else if (msg.contains('ACCOUNT_INACTIVE')) {
|
} else if (code == 'ACCOUNT_INACTIVE' || e.response?.statusCode == 403) {
|
||||||
throw Exception('Akun tidak aktif. Hubungi administrator.');
|
throw Exception('Akun tidak aktif. Hubungi administrator.');
|
||||||
}
|
}
|
||||||
throw Exception('Gagal masuk. Coba lagi.');
|
throw Exception('Gagal masuk. Coba lagi.');
|
||||||
|
} on Exception {
|
||||||
|
await _auth.signOut();
|
||||||
|
throw Exception('Gagal masuk. Coba lagi.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'auth_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$mitraAuthHash() => r'65235a41cde3a37feef0b3004a0a48b508bf9ac9';
|
String _$mitraAuthHash() => r'ddb09225b47b4e7683c9f8ad46abc21d9fb7a37b';
|
||||||
|
|
||||||
/// See also [MitraAuth].
|
/// See also [MitraAuth].
|
||||||
@ProviderFor(MitraAuth)
|
@ProviderFor(MitraAuth)
|
||||||
|
|||||||
@@ -68,6 +68,13 @@ class MitraExtension extends _$MitraExtension {
|
|||||||
'message': message,
|
'message': message,
|
||||||
});
|
});
|
||||||
state = const ExtensionCompleteData();
|
state = const ExtensionCompleteData();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
final code = e.response?.data?['error']?['code'];
|
||||||
|
if (code == 'SESSION_NOT_ACTIVE' || e.response?.statusCode == 409) {
|
||||||
|
state = const ExtensionCompleteData();
|
||||||
|
} else {
|
||||||
|
state = const ExtensionErrorData('Gagal mengirim pesan penutup.');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = const ExtensionErrorData('Gagal mengirim pesan penutup.');
|
state = const ExtensionErrorData('Gagal mengirim pesan penutup.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'status_notifier.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$onlineStatusHash() => r'6b42328eaba0f7934b0e3eaa54eb6b764f1c4e53';
|
String _$onlineStatusHash() => r'26f86241ddbe8534b8ab700d3dcaa22c5f17eb76';
|
||||||
|
|
||||||
/// See also [OnlineStatus].
|
/// See also [OnlineStatus].
|
||||||
@ProviderFor(OnlineStatus)
|
@ProviderFor(OnlineStatus)
|
||||||
|
|||||||
Reference in New Issue
Block a user