Skip to main content

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 setInterval yang di-cleanup dengan clearInterval
  • ✅ 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

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:

  1. Deskripsi perubahan
  2. Test coverage
  3. Screenshot (jika ada perubahan UI)