feat(authentication): implement frontend authentication system

- Created auth and user types (T10)
- Implemented API client with token handling (T11)
- Built AuthContext with JWT decoding (T12)
- Added ProtectedRoute component (T13)
- Created LoginPage, RegisterPage, DashboardPage (T14-T16)
- Updated App.tsx with routing and auth provider (T17)
- Added Dockerfile, nginx.conf for frontend deployment (T18-T19)
- Updated docker-compose.yml to include frontend service (T20)
- Updated .gitignore to exclude frontend build artifacts (T21)
- Removed unused App.css (T22)

Refs #2
This commit is contained in:
2026-04-01 13:37:37 -03:00
parent bf2cfbd9fc
commit 7b9a7192c1
32 changed files with 4450 additions and 1 deletions

View File

@@ -0,0 +1,113 @@
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { apiClient } from '../api/client';
import { RegisterResponse, LoginRequest, RegisterRequest } from '../types/auth';
import { User } from '../types/user';
interface AuthContextProps {
user: User | null;
token: string | null;
isAuthenticated: boolean;
login: (credentials: LoginRequest) => Promise<void>;
register: (userData: RegisterRequest) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextProps | undefined>(undefined);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
// Check for token in localStorage on initial load
useEffect(() => {
const storedToken = localStorage.getItem('token');
if (storedToken) {
setToken(storedToken);
try {
// Decode JWT payload (second part)
const payload = storedToken.split('.')[1];
const decoded = JSON.parse(atob(payload));
setUser({
id: decoded.userId,
username: decoded.username,
email: decoded.email,
nombreCompleto: decoded.nombreCompleto,
});
setIsAuthenticated(true);
} catch (error) {
console.error('Error decoding token:', error);
// If token is invalid, clear it
localStorage.removeItem('token');
setToken(null);
setUser(null);
setIsAuthenticated(false);
}
}
}, []);
const login = async (credentials: LoginRequest) => {
const response = await apiClient.login(credentials);
localStorage.setItem('token', response.token);
setToken(response.token);
// Decode JWT to get user info
const payload = response.token.split('.')[1];
const decoded = JSON.parse(atob(payload));
setUser({
id: decoded.userId,
username: decoded.username,
email: decoded.email,
nombreCompleto: decoded.nombreCompleto,
});
setIsAuthenticated(true);
};
const register = async (userData: RegisterRequest) => {
const response = await apiClient.register(userData);
localStorage.setItem('token', response.token);
setToken(response.token);
// Decode JWT to get user info
const payload = response.token.split('.')[1];
const decoded = JSON.parse(atob(payload));
setUser({
id: decoded.userId,
username: decoded.username,
email: decoded.email,
nombreCompleto: decoded.nombreCompleto,
});
setIsAuthenticated(true);
};
const logout = () => {
localStorage.removeItem('token');
setToken(null);
setUser(null);
setIsAuthenticated(false);
window.location.href = '/login';
};
const value = {
user,
token,
isAuthenticated,
login,
register,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};