AuthChecker
Component untuk monitoring session authentication secara real-time dengan dialog countdown otomatis ketika token expired.
Overview
AuthChecker adalah komponen React yang berjalan di background untuk memvalidasi token authentication secara periodik. Ketika token tidak valid atau expired, komponen ini akan menampilkan dialog dengan countdown timer yang akan otomatis logout user dan redirect ke halaman login.
Features
- ✅ Periodic Token Validation: Auto-check setiap 60 detik
- ✅ Countdown Dialog: Tampilan countdown 60 detik sebelum logout
- ✅ Public Route Handling: Exclude path tertentu dari validasi
- ✅ Auto Logout: Otomatis logout ketika countdown habis
- ✅ Manual Login: User bisa klik tombol untuk login langsung
- ✅ Route-aware: Responsive terhadap perubahan route
Installation
Dependencies
npm install lucide-react react-router-dom
Required Components
Pastikan sudah memiliki komponen UI berikut:
// UI Components (shadcn/ui atau custom)
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
Required Utils & Context
// Utils
import { getCurrentUser } from "@/data/userData";
import { isTokenExpired } from "@/utils/auth";
// Context
import { useAuth } from "@/contexts/AuthContext";
API Reference
Props
AuthChecker tidak menerima props karena menggunakan context dan utility functions internal.
Internal Configuration
const publicPaths = ["/login", "/callback"];
const checkInterval = 60 * 1000; // 60 detik
const countdownDuration = 60; // 60 detik
Usage
Basic Implementation
// App.tsx atau Root Layout
import AuthChecker from "@/components/AuthChecker";
function App() {
return (
<AuthProvider>
<Router>
<AuthChecker /> {/* Letakkan di root level */}
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
{/* ... routes lainnya */}
</Routes>
</Router>
</AuthProvider>
);
}
With Layout
// Layout.tsx
import AuthChecker from "@/components/AuthChecker";
function RootLayout({ children }) {
return (
<>
<AuthChecker />
<div className="app-container">
<Sidebar />
<main>{children}</main>
</div>
</>
);
}
Code Example
Full Implementation
import { useEffect, useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { getCurrentUser } from "@/data/userData";
import { isTokenExpired } from "@/utils/auth";
import { useAuth } from "@/contexts/AuthContext";
import { useLocation } from "react-router-dom";
import { AlertTriangle, User } from "lucide-react";
const AuthChecker = () => {
const location = useLocation();
const publicPaths = ["/login", "/callback"];
const isPublic = publicPaths.includes(location.pathname);
const [showDialog, setShowDialog] = useState(false);
const [countdown, setCountdown] = useState(60);
const { logout } = useAuth();
const checkAuthAndSync = () => {
const storedUser = getCurrentUser();
if (
(!storedUser || !storedUser.token || isTokenExpired(storedUser.token)) &&
!isPublic
) {
setShowDialog(true);
setCountdown(60);
return false;
}
return true;
};
// Check auth on mount dan route change
useEffect(() => {
checkAuthAndSync();
// Expose function ke window (optional)
(window as any).checkAuth = () => {
return checkAuthAndSync();
};
// Periodic check setiap 60 detik
const interval = setInterval(() => {
checkAuthAndSync();
}, 60 * 1000);
return () => clearInterval(interval);
}, [location.pathname]);
// Countdown timer
useEffect(() => {
if (!showDialog) return;
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
setShowDialog(false);
clearInterval(timer);
logout();
}
return prev - 1;
});
}, 1000);
return () => clearInterval(timer);
}, [showDialog]);
// Auto-hide dialog jika masuk ke public path
useEffect(() => {
!!showDialog && isPublic && setShowDialog(false);
}, [isPublic, showDialog]);
return (
<Dialog
open={showDialog}
onOpenChange={(open) => {
if (!open) {
setShowDialog(false);
logout();
}
}}>
<DialogContent className="sm:max-w-sm bg-gradient-to-b from-white to-blue-50 border-2 border-blue-200 shadow-2xl rounded-xl">
<div className="text-center space-y-6 p-6">
<div className="mx-auto flex items-center justify-center">
<AlertTriangle color="red" className="w-16 h-16 text-white" />
</div>
<DialogHeader className="space-y-3">
<DialogTitle className="text-xl font-semibold">
Sesi Berakhir
</DialogTitle>
<DialogDescription className="text-sm">
Token Anda sudah tidak valid atau telah kedaluwarsa.
</DialogDescription>
</DialogHeader>
<div className="bg-white rounded-xl p-4 border-2 border-blue-100 shadow-sm">
<div className="font-medium mb-2">
Anda akan dipindahkan ke halaman login dalam
</div>
<div className="text-3xl font-bold text-red-600 mb-1">
{countdown}
</div>
<div className="text-red-500 text-sm">detik</div>
</div>
<Button
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 text-white py-2.5 text-sm rounded-lg font-medium shadow-md"
onClick={() => {
setShowDialog(false);
logout();
}}>
<User className="w-4 h-4 mr-2" />
Login Sekarang
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default AuthChecker;
Customization
Mengubah Public Paths
const publicPaths = [
"/login",
"/callback",
"/register",
"/forgot-password",
"/verify-email"
];
Mengubah Check Interval
// Check setiap 30 detik
const interval = setInterval(() => {
checkAuthAndSync();
}, 30 * 1000);
Mengubah Countdown Duration
const checkAuthAndSync = () => {
// ... validation logic
setCountdown(30); // 30 detik countdown
};
Custom Dialog Styling
<DialogContent className="custom-dialog-class">
{/* Custom styling */}
</DialogContent>
Required Utility Functions
isTokenExpired
// utils/auth.ts
export const isTokenExpired = (token: string): boolean => {
try {
const payload = JSON.parse(atob(token.split('.')[1]));
const exp = payload.exp * 1000; // Convert to milliseconds
return Date.now() >= exp;
} catch {
return true;
}
};
getCurrentUser
// data/userData.ts
export const getCurrentUser = () => {
const userData = localStorage.getItem('user');
return userData ? JSON.parse(userData) : null;
};
useAuth Context
// contexts/AuthContext.tsx
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
};
// Context harus menyediakan:
// - logout(): void
Best Practices
1. Single Instance
Pastikan hanya ada 1 instance AuthChecker di aplikasi (letakkan di root level).
// ✅ Correct
<App>
<AuthChecker />
<Routes />
</App>
// ❌ Wrong - Multiple instances
<Route>
<AuthChecker /> {/* Jangan letakkan di setiap route */}
</Route>
2. Public Path Configuration
Selalu update publicPaths sesuai routes yang tidak butuh authentication.
const publicPaths = ["/login", "/callback", "/public/*"];
3. Error Handling
Tambahkan error boundary untuk menangani error di AuthChecker.
<ErrorBoundary>
<AuthChecker />
</ErrorBoundary>
4. Testing
Mock getCurrentUser dan isTokenExpired saat testing.
jest.mock('@/data/userData', () => ({
getCurrentUser: jest.fn()
}));
Troubleshooting
Dialog Muncul di Public Path
Problem: Dialog muncul meskipun sedang di halaman login.
Solution: Pastikan path sudah ada di publicPaths dan exact match.
const publicPaths = ["/login", "/callback"];
const isPublic = publicPaths.includes(location.pathname);
Infinite Logout Loop
Problem: User terus-terusan di-logout.
Solution: Pastikan logout() tidak memanggil re-render yang memicu checkAuthAndSync() lagi.
const logout = useCallback(() => {
localStorage.removeItem('user');
navigate('/login');
}, [navigate]);
Token Check Terlalu Sering
Problem: API endpoint untuk check token terlalu sering dipanggil.
Solution: Tingkatkan interval atau gunakan debounce.
// Dari 60 detik ke 5 menit
const interval = setInterval(() => {
checkAuthAndSync();
}, 5 * 60 * 1000);
Performance Considerations
- ✅ Menggunakan
setIntervalyang di-cleanup denganclearInterval - ✅ Dialog hanya render ketika
showDialog = true - ✅ Tidak ada re-render berlebihan karena menggunakan proper dependency array
- ✅ Countdown timer di-cleanup saat dialog close
Browser Compatibility
- ✅ Modern browsers (Chrome, Firefox, Safari, Edge)
- ✅ Requires ES6+ support
- ✅ LocalStorage API required
Related Components
- Authentication - Main authentication flow
Version History
- v1.0.0: Initial implementation dengan basic token checking
- v1.1.0: Added countdown dialog
- v1.2.0: Added public path handling
- v1.3.0: Added route-aware validation
Contributing
Untuk improvement atau bug fixes, silakan submit PR dengan:
- Deskripsi perubahan
- Test coverage
- Screenshot (jika ada perubahan UI)