taller-ionic/src/app/services/auth.service.ts
2025-04-24 15:57:53 -04:00

285 lines
9.1 KiB
TypeScript

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
import { environment } from '../../environments/environment';
import { StorageService } from './storage.service';
// Clave para almacenar información de sesión
const AUTH_TOKEN_KEY = 'auth_token';
const USER_KEY = 'user_data';
// Interfaz para credenciales de login
interface LoginCredentials {
email: string;
password: string;
}
// Interfaz para datos de registro
interface RegisterData {
name: string;
email: string;
password: string;
}
@Injectable({
providedIn: 'root'
})
export class AuthService {
private apiUrl = `${environment.apiUrl}/api/users`;
private authUrl = `${environment.apiUrl}/api/auth`;
private currentUserSubject = new BehaviorSubject<User | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
private isInitialized = false;
constructor(
private http: HttpClient,
private storageService: StorageService
) {
// Intentar recuperar sesión anterior
this.initializeSession();
}
/**
* Inicializa la sesión si hay datos guardados
*/
async initializeSession() {
if (this.isInitialized) return;
try {
const userData = await this.storageService.get(USER_KEY) as User | null;
if (userData) {
console.log('Sesión recuperada del almacenamiento local:', userData);
// Normalizar campos para compatibilidad
this.normalizeUserData(userData);
this.currentUserSubject.next(userData);
} else {
console.log('No hay sesión guardada, redirigiendo a login');
this.currentUserSubject.next(null);
}
} catch (error) {
console.error('Error al inicializar sesión:', error);
this.currentUserSubject.next(null);
}
this.isInitialized = true;
}
/**
* Espera a que se complete la inicialización
* Importante para evitar redirecciones erróneas cuando la app inicia
*/
async waitForInitialization(): Promise<void> {
if (this.isInitialized) return;
// Si no se ha inicializado, iniciamos el proceso
await this.initializeSession();
// Esperar hasta que se inicialice (con timeout)
let attempts = 0;
while (!this.isInitialized && attempts < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
}
/**
* Normaliza los datos de usuario para compatibilidad entre frontend/backend
*/
private normalizeUserData(user: User) {
// Sincronizar campos de imagen
if (user.profilePicUrl && !user.profilePic) {
user.profilePic = user.profilePicUrl;
} else if (user.profilePic && !user.profilePicUrl) {
user.profilePicUrl = user.profilePic;
}
// Sincronizar preferencias de notificaciones
if (user.notificationsEnabled !== undefined && !user.preferences) {
user.preferences = {
notifications: user.notificationsEnabled,
favoriteClasses: []
};
} else if (user.preferences?.notifications !== undefined && user.notificationsEnabled === undefined) {
user.notificationsEnabled = user.preferences.notifications;
}
}
getCurrentUser(): User | null {
return this.currentUserSubject.value;
}
/**
* Iniciar sesión con email y contraseña
*/
login(credentials: LoginCredentials): Observable<User> {
console.log('Intentando login con:', credentials);
return this.http.post<any>(`${this.authUrl}/login`, credentials).pipe(
tap(async (response) => {
console.log('Respuesta de login:', response);
// Guardar token si el backend lo devuelve
if (response.token) {
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
}
// Verificar y normalizar datos del usuario
const user = response.user || response;
this.normalizeUserData(user);
// Guardar datos del usuario
await this.storageService.set(USER_KEY, user);
// Actualizar estado de sesión
this.currentUserSubject.next(user);
}),
map(response => response.user || response)
);
}
/**
* Registrar un nuevo usuario
*/
register(userData: RegisterData): Observable<User> {
console.log('Registrando usuario:', userData);
const registerData = {
name: userData.name,
email: userData.email,
password: userData.password
};
return this.http.post<any>(`${this.authUrl}/register`, registerData).pipe(
tap(async (response) => {
console.log('Respuesta de registro:', response);
// Guardar token si el backend lo devuelve
if (response.token) {
await this.storageService.set(AUTH_TOKEN_KEY, response.token);
}
// Verificar y normalizar datos del usuario
const user = response.user || response;
this.normalizeUserData(user);
// Guardar datos del usuario
await this.storageService.set(USER_KEY, user);
// Actualizar estado de sesión
this.currentUserSubject.next(user);
}),
map(response => response.user || response)
);
}
/**
* Método para simular login/registro en una app de demo
*/
simulateAuth(userData: User): Observable<User> {
return of(userData).pipe(
tap(async (user) => {
this.normalizeUserData(user);
await this.storageService.set(USER_KEY, user);
this.currentUserSubject.next(user);
})
);
}
updateUserProfile(userData: Partial<User>): Observable<User> {
const currentUser = this.getCurrentUser();
if (!currentUser) {
return of(currentUser as unknown as User);
}
console.log('Actualizando perfil de usuario con:', userData);
// Asegurar que ambos campos de imagen estén sincronizados
if (userData.profilePic && !userData.profilePicUrl) {
userData.profilePicUrl = userData.profilePic;
} else if (userData.profilePicUrl && !userData.profilePic) {
userData.profilePic = userData.profilePicUrl;
}
// Sincronizar notificaciones entre los dos formatos
if (userData.preferences?.notifications !== undefined && userData.notificationsEnabled === undefined) {
userData.notificationsEnabled = userData.preferences.notifications;
} else if (userData.notificationsEnabled !== undefined &&
(!userData.preferences || userData.preferences.notifications === undefined)) {
if (!userData.preferences) userData.preferences = { notifications: false, favoriteClasses: [] };
userData.preferences.notifications = userData.notificationsEnabled;
}
// Solo enviamos al servidor los campos que espera
const backendUserData = {
name: userData.name,
profilePicUrl: userData.profilePicUrl,
notificationsEnabled: userData.notificationsEnabled ||
(userData.preferences ? userData.preferences.notifications : undefined)
};
return this.http.put<User>(`${this.apiUrl}/${currentUser.id}`, backendUserData).pipe(
tap(async (user) => {
console.log('Usuario actualizado correctamente:', user);
// Sincronizar campos para mantener compatibilidad
if (user.profilePicUrl && !user.profilePic) {
user.profilePic = user.profilePicUrl;
} else if (user.profilePic && !user.profilePicUrl) {
user.profilePicUrl = user.profilePic;
}
// Mantener los campos que espera el frontend
if (user.notificationsEnabled !== undefined && !user.preferences) {
user.preferences = {
notifications: user.notificationsEnabled,
favoriteClasses: currentUser.preferences?.favoriteClasses || []
};
}
// Actualizar almacenamiento local
await this.storageService.set(USER_KEY, user);
this.currentUserSubject.next(user);
}),
catchError(error => {
console.error('Error al actualizar usuario:', error);
// Devolver el usuario actualizado localmente para que la UI no se rompa
// en caso de error de red
const updatedUser = { ...currentUser, ...userData };
this.currentUserSubject.next(updatedUser);
return of(updatedUser);
})
);
}
/**
* Cerrar sesión y eliminar datos
*/
async logout(): Promise<boolean> {
try {
// En una aplicación real, enviaríamos petición al backend
// this.http.post(`${this.authUrl}/logout`, {}).subscribe();
// Limpiar datos de sesión
await this.storageService.clear(); // Limpiamos TODA la caché de preferencias
// Actualizar estado
this.currentUserSubject.next(null);
this.isInitialized = false; // Permitir nueva inicialización
return true;
} catch (error) {
console.error('Error al cerrar sesión:', error);
return false;
}
}
/**
* Verificar si hay sesión activa
*/
isLoggedIn(): boolean {
return !!this.currentUserSubject.value;
}
}