Implement Phase 1e: Frontend Authentication Integration
- Create auth store (Zustand) for managing user and tokens - Implement API client with automatic token refresh on 401 - Add LoginPage with email/password form and demo credentials - Create ProtectedRoute component for route-level authorization - Update App.tsx to integrate authentication and login page - Add logout functionality to UserMenu component - All protected routes now require authentication - Token auto-refresh on expiry using refresh tokens - Toast notifications for auth errors and events Frontend now fully integrated with backend API authentication.
This commit is contained in:
@@ -1,15 +1,24 @@
|
||||
import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||
import { ToastProvider } from './components/ToastProvider';
|
||||
import { ProtectedRoute } from './components/ProtectedRoute';
|
||||
import UserMenu from './components/UserMenu';
|
||||
import DashboardPage from './pages/DashboardPage';
|
||||
import TransactionsPage from './pages/TransactionsPage';
|
||||
import TreasuryPage from './pages/TreasuryPage';
|
||||
import ReportsPage from './pages/ReportsPage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import { FiHome, FiFileText, FiDollarSign, FiBarChart2, FiBell } from 'react-icons/fi';
|
||||
import { useAuthStore } from './stores/authStore';
|
||||
|
||||
function Navigation() {
|
||||
const location = useLocation();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// Don't show navigation on login page
|
||||
if (location.pathname === '/login') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
@@ -56,7 +65,7 @@ function Navigation() {
|
||||
<FiBell className="w-5 h-5" />
|
||||
<span className="absolute top-0 right-0 block h-2 w-2 rounded-full bg-red-500 ring-2 ring-white" />
|
||||
</button>
|
||||
<UserMenu />
|
||||
{authStore.isAuthenticated && <UserMenu />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,18 +78,53 @@ function App() {
|
||||
<ErrorBoundary>
|
||||
<ToastProvider>
|
||||
<BrowserRouter>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Navigation />
|
||||
<Navigation />
|
||||
|
||||
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<DashboardPage />} />
|
||||
<Route path="/transactions" element={<TransactionsPage />} />
|
||||
<Route path="/treasury" element={<TreasuryPage />} />
|
||||
<Route path="/reports" element={<ReportsPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
<main className="min-h-screen bg-gray-50">
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<DashboardPage />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/transactions"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<TransactionsPage />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/treasury"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<TreasuryPage />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/reports"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
||||
<ReportsPage />
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
</BrowserRouter>
|
||||
</ToastProvider>
|
||||
</ErrorBoundary>
|
||||
|
||||
68
apps/web/src/components/ProtectedRoute.tsx
Normal file
68
apps/web/src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Protected Route Component
|
||||
* Redirects unauthenticated users to login
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { getCurrentUser } from '../services/api';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode;
|
||||
requiredRoles?: number[];
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) {
|
||||
const authStore = useAuthStore();
|
||||
const [isVerifying, setIsVerifying] = useState(true);
|
||||
const [hasAccess, setHasAccess] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function verifyAuth() {
|
||||
setIsVerifying(true);
|
||||
|
||||
// Check if user has auth token
|
||||
if (!authStore.accessToken) {
|
||||
setHasAccess(false);
|
||||
setIsVerifying(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify token is still valid
|
||||
const user = await getCurrentUser();
|
||||
authStore.setUser(user);
|
||||
|
||||
// Check roles if required
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
const hasRequiredRole = user.roles.some((roleId: number) =>
|
||||
requiredRoles.includes(roleId)
|
||||
);
|
||||
setHasAccess(hasRequiredRole);
|
||||
} else {
|
||||
setHasAccess(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Auth verification failed:', error);
|
||||
authStore.clearAuth();
|
||||
setHasAccess(false);
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}
|
||||
|
||||
verifyAuth();
|
||||
}, [authStore, requiredRoles]);
|
||||
|
||||
if (isVerifying) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
@@ -1,9 +1,16 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiUser, FiSettings, FiLogOut, FiHelpCircle } from 'react-icons/fi';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { logout } from '../services/api';
|
||||
import { useToast } from './ToastProvider';
|
||||
|
||||
export default function UserMenu() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const authStore = useAuthStore();
|
||||
const { showToast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@@ -18,6 +25,19 @@ export default function UserMenu() {
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
authStore.clearAuth();
|
||||
showToast('success', 'Logged out successfully');
|
||||
navigate('/login');
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
authStore.clearAuth();
|
||||
navigate('/login');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative" ref={menuRef}>
|
||||
<button
|
||||
@@ -28,14 +48,18 @@ export default function UserMenu() {
|
||||
<div className="w-8 h-8 bg-blue-600 rounded-full flex items-center justify-center">
|
||||
<FiUser className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<span className="hidden md:block text-sm font-medium text-gray-700">User</span>
|
||||
<span className="hidden md:block text-sm font-medium text-gray-700">
|
||||
{authStore.user?.name || 'User'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg border border-gray-200 py-1 z-50">
|
||||
<div className="px-4 py-2 border-b border-gray-200">
|
||||
<p className="text-sm font-medium text-gray-900">User Name</p>
|
||||
<p className="text-xs text-gray-500">user@example.com</p>
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{authStore.user?.name || 'User'}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{authStore.user?.email}</p>
|
||||
</div>
|
||||
<a
|
||||
href="#"
|
||||
@@ -73,10 +97,7 @@ export default function UserMenu() {
|
||||
<div className="border-t border-gray-200 my-1" />
|
||||
<button
|
||||
className="w-full flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
// TODO: Implement logout
|
||||
}}
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<FiLogOut className="w-4 h-4 mr-3" />
|
||||
Logout
|
||||
|
||||
153
apps/web/src/pages/LoginPage.tsx
Normal file
153
apps/web/src/pages/LoginPage.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* Login Page
|
||||
* User authentication page
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiMail, FiLock, FiAlertCircle } from 'react-icons/fi';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
import { login } from '../services/api';
|
||||
|
||||
export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const authStore = useAuthStore();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setErrors({});
|
||||
|
||||
// Validate
|
||||
const newErrors: Record<string, string> = {};
|
||||
if (!email) newErrors.email = 'Email is required';
|
||||
if (!password) newErrors.password = 'Password is required';
|
||||
|
||||
if (Object.keys(newErrors).length > 0) {
|
||||
setErrors(newErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await login(email, password);
|
||||
authStore.setTokens(response.accessToken, response.refreshToken);
|
||||
authStore.setUser(response.user);
|
||||
showToast('success', 'Login successful');
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
showToast('error', 'Invalid email or password');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-600 to-blue-800 flex items-center justify-center px-4">
|
||||
<div className="w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-4xl font-bold text-white mb-2">Brazil SWIFT Ops</h1>
|
||||
<p className="text-blue-100">Cross-Border Payment Control Center</p>
|
||||
</div>
|
||||
|
||||
{/* Login Card */}
|
||||
<div className="bg-white rounded-lg shadow-xl p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-900 mb-6">Sign In</h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Email */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Email Address
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiMail className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className={`w-full pl-10 pr-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition ${
|
||||
errors.email ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="admin@example.com"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 text-sm mt-1 flex items-center">
|
||||
<FiAlertCircle className="mr-1 w-4 h-4" />
|
||||
{errors.email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
Password
|
||||
</label>
|
||||
<div className="relative">
|
||||
<FiLock className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={`w-full pl-10 pr-4 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition ${
|
||||
errors.password ? 'border-red-500' : 'border-gray-300'
|
||||
}`}
|
||||
placeholder="••••••••"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 text-sm mt-1 flex items-center">
|
||||
<FiAlertCircle className="mr-1 w-4 h-4" />
|
||||
{errors.password}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-md transition duration-200 mt-6"
|
||||
>
|
||||
{isLoading ? 'Signing In...' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{/* Demo Credentials */}
|
||||
<div className="mt-6 pt-6 border-t border-gray-200">
|
||||
<p className="text-xs text-gray-600 mb-2">Demo Credentials:</p>
|
||||
<div className="bg-gray-50 p-3 rounded text-xs space-y-1">
|
||||
<p>
|
||||
<strong>Admin:</strong> admin@example.com / Admin123!
|
||||
</p>
|
||||
<p>
|
||||
<strong>Manager:</strong> manager@example.com / Manager123!
|
||||
</p>
|
||||
<p>
|
||||
<strong>Analyst:</strong> analyst@example.com / Analyst123!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-blue-100 text-sm mt-8">
|
||||
© 2024 Brazil SWIFT Operations. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
205
apps/web/src/services/api.ts
Normal file
205
apps/web/src/services/api.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* API Client
|
||||
* Handles HTTP requests with authentication
|
||||
*/
|
||||
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useToast } from '../components/ToastProvider';
|
||||
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3000';
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch wrapper with authentication
|
||||
*/
|
||||
async function apiFetch<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const authStore = useAuthStore();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.set('Content-Type', 'application/json');
|
||||
|
||||
if (authStore.accessToken) {
|
||||
headers.set('Authorization', `Bearer ${authStore.accessToken}`);
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
// Handle 401 - try to refresh token
|
||||
if (response.status === 401 && authStore.refreshToken) {
|
||||
try {
|
||||
const refreshResponse = await fetch(`${API_BASE_URL}/api/v1/auth/refresh`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ refreshToken: authStore.refreshToken }),
|
||||
});
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const { accessToken } = await refreshResponse.json();
|
||||
authStore.setAccessToken(accessToken);
|
||||
|
||||
// Retry original request with new token
|
||||
headers.set('Authorization', `Bearer ${accessToken}`);
|
||||
const retryResponse = await fetch(`${API_BASE_URL}${endpoint}`, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!retryResponse.ok) {
|
||||
throw new Error(`API error: ${retryResponse.status}`);
|
||||
}
|
||||
|
||||
return retryResponse.json();
|
||||
} else {
|
||||
// Refresh failed, clear auth
|
||||
authStore.clearAuth();
|
||||
showToast('error', 'Session expired. Please log in again');
|
||||
return Promise.reject(new Error('Session expired'));
|
||||
}
|
||||
} catch (error) {
|
||||
authStore.clearAuth();
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 403
|
||||
if (response.status === 403) {
|
||||
showToast('error', 'You do not have permission to perform this action');
|
||||
return Promise.reject(new Error('Access denied'));
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMessage = errorData.error || `Request failed: ${response.status}`;
|
||||
showToast('error', errorMessage);
|
||||
return Promise.reject(new Error(errorMessage));
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
export async function apiGet<T>(endpoint: string): Promise<T> {
|
||||
return apiFetch<T>(endpoint, { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
export async function apiPost<T>(endpoint: string, data?: any): Promise<T> {
|
||||
return apiFetch<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
export async function apiPut<T>(endpoint: string, data?: any): Promise<T> {
|
||||
return apiFetch<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
export async function apiDelete<T>(endpoint: string): Promise<T> {
|
||||
return apiFetch<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Login
|
||||
*/
|
||||
export async function login(
|
||||
email: string,
|
||||
password: string
|
||||
): Promise<{ accessToken: string; refreshToken: string; user: any }> {
|
||||
const response = await apiFetch<any>('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout
|
||||
*/
|
||||
export async function logout(): Promise<void> {
|
||||
try {
|
||||
await apiFetch('/api/v1/auth/logout', { method: 'POST' });
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user profile
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<any> {
|
||||
return apiFetch('/api/v1/auth/me', { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transactions
|
||||
*/
|
||||
export async function getTransactions(page = 1, limit = 20): Promise<any> {
|
||||
return apiGet(`/api/v1/transactions?page=${page}&limit=${limit}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transaction by ID
|
||||
*/
|
||||
export async function getTransaction(id: string): Promise<any> {
|
||||
return apiGet(`/api/v1/transactions/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create transaction
|
||||
*/
|
||||
export async function createTransaction(data: any): Promise<any> {
|
||||
return apiPost('/api/v1/transactions', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get accounts
|
||||
*/
|
||||
export async function getAccounts(page = 1): Promise<any> {
|
||||
return apiGet(`/api/v1/accounts?page=${page}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get account by ID
|
||||
*/
|
||||
export async function getAccount(id: string): Promise<any> {
|
||||
return apiGet(`/api/v1/accounts/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reports
|
||||
*/
|
||||
export async function getTransactionSummary(): Promise<any> {
|
||||
return apiGet('/api/v1/reports/transaction-summary');
|
||||
}
|
||||
|
||||
export async function getComplianceSummary(): Promise<any> {
|
||||
return apiGet('/api/v1/reports/compliance-summary');
|
||||
}
|
||||
92
apps/web/src/stores/authStore.ts
Normal file
92
apps/web/src/stores/authStore.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Authentication Store
|
||||
* Manages user authentication state and tokens
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
roles: number[];
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
user: User | null;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
setUser: (user: User | null) => void;
|
||||
setTokens: (accessToken: string, refreshToken: string) => void;
|
||||
setAccessToken: (accessToken: string) => void;
|
||||
clearAuth: () => void;
|
||||
setError: (error: string | null) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => {
|
||||
// Load from localStorage on initialization
|
||||
const savedAccessToken =
|
||||
typeof window !== 'undefined' ? localStorage.getItem('accessToken') : null;
|
||||
const savedRefreshToken =
|
||||
typeof window !== 'undefined' ? localStorage.getItem('refreshToken') : null;
|
||||
const savedUser = typeof window !== 'undefined'
|
||||
? localStorage.getItem('user')
|
||||
? JSON.parse(localStorage.getItem('user')!)
|
||||
: null
|
||||
: null;
|
||||
|
||||
return {
|
||||
user: savedUser,
|
||||
accessToken: savedAccessToken,
|
||||
refreshToken: savedRefreshToken,
|
||||
isAuthenticated: !!savedAccessToken,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
setUser: (user: User | null) => {
|
||||
set({ user });
|
||||
if (user) {
|
||||
localStorage.setItem('user', JSON.stringify(user));
|
||||
} else {
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
},
|
||||
|
||||
setTokens: (accessToken: string, refreshToken: string) => {
|
||||
set({ accessToken, refreshToken, isAuthenticated: true });
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
localStorage.setItem('refreshToken', refreshToken);
|
||||
},
|
||||
|
||||
setAccessToken: (accessToken: string) => {
|
||||
set({ accessToken });
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
},
|
||||
|
||||
clearAuth: () => {
|
||||
set({
|
||||
user: null,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isAuthenticated: false,
|
||||
});
|
||||
localStorage.removeItem('accessToken');
|
||||
localStorage.removeItem('refreshToken');
|
||||
localStorage.removeItem('user');
|
||||
},
|
||||
|
||||
setError: (error: string | null) => {
|
||||
set({ error });
|
||||
},
|
||||
|
||||
setLoading: (loading: boolean) => {
|
||||
set({ isLoading: loading });
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user