feat: implement user authentication and login modal, refactor backend

This commit is contained in:
Omar Sánchez Pizarro
2026-01-20 00:39:28 +01:00
parent 9a61f16959
commit e99424c9ba
29 changed files with 3061 additions and 855 deletions

View File

@@ -1,4 +1,5 @@
import axios from 'axios';
import authService from './auth';
const api = axios.create({
baseURL: '/api',
@@ -7,6 +8,37 @@ const api = axios.create({
},
});
// Interceptor para añadir autenticación a las peticiones
api.interceptors.request.use(
(config) => {
const authHeader = authService.getAuthHeader();
if (authHeader) {
config.headers.Authorization = authHeader;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Interceptor para manejar errores 401 (no autenticado)
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Disparar evento personalizado para mostrar diálogo de login
window.dispatchEvent(new CustomEvent('auth-required', {
detail: {
message: 'Se requiere autenticación para esta acción',
config: error.config
}
}));
}
return Promise.reject(error);
}
);
export default {
// Estadísticas
async getStats() {
@@ -83,5 +115,26 @@ export default {
const response = await api.delete('/cache');
return response.data;
},
// Usuarios
async getUsers() {
const response = await api.get('/users');
return response.data;
},
async createUser(userData) {
const response = await api.post('/users', userData);
return response.data;
},
async deleteUser(username) {
const response = await api.delete(`/users/${username}`);
return response.data;
},
async changePassword(passwordData) {
const response = await api.post('/users/change-password', passwordData);
return response.data;
},
};

View File

@@ -0,0 +1,95 @@
// Servicio de autenticación para gestionar credenciales
const AUTH_STORAGE_KEY = 'wallabicher_auth';
class AuthService {
constructor() {
this.credentials = this.loadCredentials();
}
// Cargar credenciales desde localStorage
loadCredentials() {
try {
const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
return {
username: parsed.username || '',
password: parsed.password || '',
};
}
} catch (error) {
console.error('Error cargando credenciales:', error);
}
return { username: '', password: '' };
}
// Guardar credenciales en localStorage
saveCredentials(username, password) {
try {
this.credentials = { username, password };
localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(this.credentials));
return true;
} catch (error) {
console.error('Error guardando credenciales:', error);
return false;
}
}
// Eliminar credenciales
clearCredentials() {
try {
this.credentials = { username: '', password: '' };
localStorage.removeItem(AUTH_STORAGE_KEY);
return true;
} catch (error) {
console.error('Error eliminando credenciales:', error);
return false;
}
}
// Obtener credenciales actuales
getCredentials() {
return { ...this.credentials };
}
// Verificar si hay credenciales guardadas
hasCredentials() {
return !!(this.credentials.username && this.credentials.password);
}
// Generar header de autenticación Basic
getAuthHeader() {
if (!this.hasCredentials()) {
return null;
}
const { username, password } = this.credentials;
const encoded = btoa(`${username}:${password}`);
return `Basic ${encoded}`;
}
// Validar credenciales (test básico)
async validateCredentials(username, password) {
try {
// Intentar hacer una petición simple para validar las credenciales
const encoded = btoa(`${username}:${password}`);
const response = await fetch('/api/stats', {
method: 'GET',
headers: {
'Authorization': `Basic ${encoded}`,
},
});
// Si la petición funciona, las credenciales son válidas
// Nota: stats no requiere auth, pero podemos usar cualquier endpoint
return response.ok || response.status !== 401;
} catch (error) {
return false;
}
}
}
// Exportar instancia singleton
const authService = new AuthService();
export default authService;