Compare commits
No commits in common. "c84c9a95c879d5039a6992b1a612b3695d7de57b" and "ea91f1c8f0844cc2dfb86d7a7f2a9d837b0e5ca6" have entirely different histories.
c84c9a95c8
...
ea91f1c8f0
@ -3,7 +3,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"newProjectRoot": "projects",
|
"newProjectRoot": "projects",
|
||||||
"projects": {
|
"projects": {
|
||||||
"correo": {
|
"cronogramas": {
|
||||||
"projectType": "application",
|
"projectType": "application",
|
||||||
"schematics": {
|
"schematics": {
|
||||||
"@schematics/angular:component": {
|
"@schematics/angular:component": {
|
||||||
@ -17,7 +17,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:application",
|
"builder": "@angular-devkit/build-angular:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/correo",
|
"outputPath": "dist/cronogramas",
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
@ -82,10 +82,10 @@
|
|||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular-devkit/build-angular:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "correo:build:production"
|
"buildTarget": "cronogramas:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "correo:build:development"
|
"buildTarget": "cronogramas:build:development"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
|
|||||||
1652
package-lock.json
generated
1652
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "correo",
|
"name": "cronogramas",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.x"
|
"node": ">=18.x"
|
||||||
@ -27,8 +27,6 @@
|
|||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"keycloak-angular": "^19.0.2",
|
|
||||||
"keycloak-js": "^26.2.0",
|
|
||||||
"pdfmake": "^0.2.19",
|
"pdfmake": "^0.2.19",
|
||||||
"primeflex": "^4.0.0",
|
"primeflex": "^4.0.0",
|
||||||
"primeicons": "^7.0.0",
|
"primeicons": "^7.0.0",
|
||||||
@ -53,5 +51,5 @@
|
|||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.7.2"
|
"typescript": "~5.7.2"
|
||||||
},
|
},
|
||||||
"description": "correo - Proyecto generado desde template"
|
"description": "cronogramas - Proyecto generado desde template"
|
||||||
}
|
}
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"realm": "aangular-app",
|
|
||||||
"auth-server-url": "http://localhost:8080/",
|
|
||||||
"resource": "angular-app",
|
|
||||||
"credentials": {
|
|
||||||
"secret": "zYbODELDmLjK9c9gHNbTUe8mSZlcLFZm"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
<html>
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
parent.postMessage(location.href, location.origin);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,108 +1,15 @@
|
|||||||
import { Component, inject, OnInit, OnDestroy, effect, Signal } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
|
import { RouterOutlet } from '@angular/router';
|
||||||
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
import { ConfirmDialogModule } from 'primeng/confirmdialog';
|
||||||
import { ConfirmationService } from 'primeng/api';
|
import { ConfirmationService } from 'primeng/api';
|
||||||
import { filter, Subscription } from 'rxjs';
|
|
||||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
||||||
import Keycloak from 'keycloak-js';
|
|
||||||
import { AuthService } from './services/auth.service';
|
|
||||||
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType, KeycloakEvent } from 'keycloak-angular';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, ConfirmDialogModule],
|
imports: [RouterOutlet, ConfirmDialogModule],
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
providers: [ConfirmationService],
|
providers: [ConfirmationService]
|
||||||
standalone: true,
|
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit, OnDestroy {
|
export class AppComponent {
|
||||||
title = 'SACG - Sistema Administrador de Cronogramas';
|
title = 'SACG - Sistema Administrador de Cronogramas';
|
||||||
private keycloak = inject(Keycloak);
|
|
||||||
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
|
|
||||||
private authService = inject(AuthService);
|
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
private subscriptions: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// Using effect to handle Keycloak events through Angular's Signal API
|
|
||||||
effect(() => {
|
|
||||||
const event = this.keycloakEvents();
|
|
||||||
if (!event) return;
|
|
||||||
|
|
||||||
console.log('Keycloak event received:', event.type);
|
|
||||||
|
|
||||||
// Authentication success handling
|
|
||||||
if (event.type === KeycloakEventType.AuthSuccess) {
|
|
||||||
console.log('Authentication successful');
|
|
||||||
// We'll let the guards and login component handle redirections
|
|
||||||
// No redirect here to avoid conflicts
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication error handling
|
|
||||||
if (event.type === KeycloakEventType.AuthError) {
|
|
||||||
console.error('Authentication error');
|
|
||||||
// Only redirect to login if not already there
|
|
||||||
if (this.router.url !== '/login') {
|
|
||||||
this.router.navigate(['/login']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logout handling
|
|
||||||
if (event.type === KeycloakEventType.AuthLogout) {
|
|
||||||
console.log('Logged out');
|
|
||||||
// Only redirect to login if not already there
|
|
||||||
if (this.router.url !== '/login') {
|
|
||||||
this.router.navigate(['/login']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
// Subscribe to navigation events - using standard unsubscribe pattern
|
|
||||||
this.subscriptions.push(
|
|
||||||
this.router.events
|
|
||||||
.pipe(
|
|
||||||
filter(event => event instanceof NavigationEnd)
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
console.log('Navigation completed to:', this.router.url);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check authentication status on load
|
|
||||||
this.checkAuthenticationStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
// Unsubscribe from any remaining subscriptions
|
|
||||||
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkAuthenticationStatus(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const isLoggedIn = await this.keycloak.authenticated;
|
|
||||||
console.log('Initial authentication status:', isLoggedIn ? 'Authenticated' : 'Not authenticated');
|
|
||||||
|
|
||||||
// Let the guards handle the protected routes
|
|
||||||
// Only do minimal checks here to avoid redirect loops
|
|
||||||
|
|
||||||
// If the user is on login page but already authenticated, send to home
|
|
||||||
if (isLoggedIn && this.router.url === '/login') {
|
|
||||||
console.log('Already authenticated, redirecting from login to home');
|
|
||||||
this.router.navigate(['/inicio']);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not authenticated and on a protected route, go to Keycloak login
|
|
||||||
if (!isLoggedIn && this.router.url !== '/login') {
|
|
||||||
// We'll let the auth guard handle this
|
|
||||||
console.log('Not authenticated on protected route');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking authentication status:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,30 +1,13 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
||||||
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router';
|
||||||
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||||
import { providePrimeNG } from 'primeng/config';
|
import { providePrimeNG } from 'primeng/config';
|
||||||
import Aura from '@primeng/themes/aura';
|
import Aura from '@primeng/themes/aura';
|
||||||
|
import { authInterceptor } from './interceptors/auth.interceptor';
|
||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import {
|
|
||||||
provideKeycloak,
|
|
||||||
createInterceptorCondition,
|
|
||||||
IncludeBearerTokenCondition,
|
|
||||||
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
|
||||||
includeBearerTokenInterceptor
|
|
||||||
} from 'keycloak-angular';
|
|
||||||
|
|
||||||
// Define condition for including the token in requests
|
|
||||||
const localhostCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
|
|
||||||
urlPattern: /^(http:\/\/localhost)(\/.*)?$/i, // Match URLs starting with http://localhost
|
|
||||||
bearerPrefix: 'Bearer'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define another condition for API URLs
|
|
||||||
const apiCondition = createInterceptorCondition<IncludeBearerTokenCondition>({
|
|
||||||
urlPattern: /^(\/api)(\/.*)?$/i, // Match URLs starting with /api
|
|
||||||
bearerPrefix: 'Bearer'
|
|
||||||
});
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -34,11 +17,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
withPreloading(PreloadAllModules)
|
withPreloading(PreloadAllModules)
|
||||||
),
|
),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
// Use the Keycloak interceptor to attach authentication tokens
|
provideHttpClient(withFetch(), withInterceptors([authInterceptor])),
|
||||||
provideHttpClient(
|
|
||||||
withFetch(),
|
|
||||||
withInterceptors([includeBearerTokenInterceptor])
|
|
||||||
),
|
|
||||||
providePrimeNG({
|
providePrimeNG({
|
||||||
theme: {
|
theme: {
|
||||||
preset: Aura,
|
preset: Aura,
|
||||||
@ -47,28 +26,6 @@ export const appConfig: ApplicationConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
MessageService,
|
MessageService
|
||||||
// Provide Keycloak with initOptions - this automatically creates an APP_INITIALIZER
|
|
||||||
provideKeycloak({
|
|
||||||
config: {
|
|
||||||
url: 'http://localhost:8080', // Asegúrate de que esta URL coincida con tu servidor Keycloak
|
|
||||||
realm: 'angular-app', // Asegúrate de que este realm existe en tu servidor Keycloak
|
|
||||||
clientId: 'angular-app',
|
|
||||||
},
|
|
||||||
initOptions: {
|
|
||||||
onLoad: 'check-sso', // Cambiado de check-sso a login-required para forzar login
|
|
||||||
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
|
|
||||||
checkLoginIframe: false,
|
|
||||||
pkceMethod: 'S256',
|
|
||||||
enableLogging: true
|
|
||||||
},
|
|
||||||
// Configure the interceptor by providing the URLs where to include the token
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
|
|
||||||
useValue: [localhostCondition, apiCondition]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,13 +5,7 @@ import { authGuard } from './guards/auth.guard';
|
|||||||
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
import { NotFoundComponent } from './pages/not-found/not-found.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
// Public routes that don't require authentication
|
{ path: 'login', component: LoginComponent },
|
||||||
{
|
|
||||||
path: 'login',
|
|
||||||
component: LoginComponent,
|
|
||||||
data: { title: 'Login' }
|
|
||||||
},
|
|
||||||
// Protected routes that require authentication
|
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
component: LayoutComponent,
|
component: LayoutComponent,
|
||||||
@ -56,4 +50,5 @@ export const routes: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ path: '**', redirectTo: '404' }
|
{ path: '**', redirectTo: '404' }
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -1,57 +1,16 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn, Router } from '@angular/router';
|
import { CanActivateFn, Router } from '@angular/router';
|
||||||
import Keycloak from 'keycloak-js';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
|
||||||
import { createAuthGuard, AuthGuardData } from 'keycloak-angular';
|
|
||||||
|
|
||||||
// Simple implementation for authentication guard
|
export const authGuard: CanActivateFn = () => {
|
||||||
export const authGuard: CanActivateFn = async (
|
const authService = inject(AuthService);
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot
|
|
||||||
): Promise<boolean | UrlTree> => {
|
|
||||||
const keycloak = inject(Keycloak);
|
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if user is authenticated
|
|
||||||
const authenticated = await keycloak.authenticated;
|
|
||||||
|
|
||||||
if (authenticated) {
|
|
||||||
console.log('User is authenticated, allowing access to protected route');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not authenticated, redirect to login
|
|
||||||
console.log('User not authenticated, redirecting to login page');
|
|
||||||
return router.createUrlTree(['/login'], {
|
|
||||||
queryParams: { returnUrl: state.url !== '/' ? state.url : '/inicio' }
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking authentication:', error);
|
|
||||||
// Fallback to login on error
|
|
||||||
return router.createUrlTree(['/login']);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Alternative implementation using the helper function from keycloak-angular
|
if (authService.isLoggedIn()) {
|
||||||
const isAccessAllowed = async (
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot,
|
|
||||||
authData: AuthGuardData
|
|
||||||
): Promise<boolean | UrlTree> => {
|
|
||||||
const { authenticated } = authData;
|
|
||||||
|
|
||||||
if (authenticated) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the URL the user was trying to access
|
// Redirect to login if not authenticated
|
||||||
const returnUrl = state.url;
|
router.navigate(['/login']);
|
||||||
const router = inject(Router);
|
return false;
|
||||||
|
};
|
||||||
// Redirect to login page with return URL
|
|
||||||
return router.createUrlTree(['/login'], { queryParams: { returnUrl } });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional: Use the createAuthGuard helper if needed
|
|
||||||
export const authGuardWithHelper = createAuthGuard(isAccessAllowed);
|
|
||||||
@ -5,53 +5,42 @@ import {
|
|||||||
HttpInterceptorFn,
|
HttpInterceptorFn,
|
||||||
HttpErrorResponse
|
HttpErrorResponse
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { Observable, throwError, from, switchMap } from 'rxjs';
|
import { Observable, throwError } from 'rxjs';
|
||||||
import { catchError } from 'rxjs/operators';
|
import { catchError } from 'rxjs/operators';
|
||||||
import Keycloak from 'keycloak-js';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
|
||||||
* Note: This interceptor is not strictly necessary when using keycloak-angular's
|
|
||||||
* built-in includeBearerTokenInterceptor, which is configured in app.config.ts.
|
|
||||||
* It's included here to provide additional error handling functionality.
|
|
||||||
*/
|
|
||||||
export const authInterceptor: HttpInterceptorFn = (
|
export const authInterceptor: HttpInterceptorFn = (
|
||||||
request: HttpRequest<unknown>,
|
request: HttpRequest<unknown>,
|
||||||
next: HttpHandlerFn
|
next: HttpHandlerFn
|
||||||
): Observable<any> => {
|
): Observable<any> => {
|
||||||
const keycloak = inject(Keycloak);
|
const authService = inject(AuthService);
|
||||||
const router = inject(Router);
|
const router = inject(Router);
|
||||||
|
|
||||||
// Handle the request with error handling for auth issues
|
// Get the auth token
|
||||||
return next(request).pipe(
|
const token = authService.getToken();
|
||||||
catchError((error: HttpErrorResponse) => {
|
|
||||||
// Handle 401 Unauthorized errors
|
// Clone the request and add the token if it exists
|
||||||
if (error.status === 401) {
|
if (token) {
|
||||||
console.log('401 Unauthorized error, refreshing token or redirecting to login');
|
const authRequest = request.clone({
|
||||||
|
setHeaders: {
|
||||||
// Try to refresh the token first
|
Authorization: `Bearer ${token}`
|
||||||
return from(keycloak.updateToken(30)).pipe(
|
|
||||||
switchMap(refreshed => {
|
|
||||||
if (refreshed) {
|
|
||||||
// Token was refreshed, retry the request
|
|
||||||
return next(request);
|
|
||||||
} else {
|
|
||||||
// Token couldn't be refreshed, redirect to login
|
|
||||||
keycloak.login();
|
|
||||||
return throwError(() => error);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
catchError(refreshError => {
|
|
||||||
console.error('Error refreshing token:', refreshError);
|
|
||||||
// Redirect to login in case of refresh error
|
|
||||||
router.navigate(['/login']);
|
|
||||||
return throwError(() => error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
// For other errors, just pass them through
|
|
||||||
return throwError(() => error);
|
// Handle the authenticated request
|
||||||
})
|
return next(authRequest).pipe(
|
||||||
);
|
catchError((error: HttpErrorResponse) => {
|
||||||
|
// Handle 401 Unauthorized errors by logging out and redirecting to login
|
||||||
|
if (error.status === 401) {
|
||||||
|
authService.logout();
|
||||||
|
router.navigate(['/login']);
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no token, just pass the request through
|
||||||
|
return next(request);
|
||||||
};
|
};
|
||||||
@ -21,36 +21,94 @@
|
|||||||
<!-- Contenedor principal con posición relativa -->
|
<!-- Contenedor principal con posición relativa -->
|
||||||
<div class="position-relative overflow-hidden">
|
<div class="position-relative overflow-hidden">
|
||||||
|
|
||||||
<!-- PANEL DE LOGIN CON KEYCLOAK -->
|
<!-- PANEL DE LOGIN -->
|
||||||
<div class="panel-container w-full animate__animated animate__fadeIn">
|
<div class="panel-container w-full"
|
||||||
|
[ngClass]="{'animate__animated animate__fadeOut d-none': showRecovery,
|
||||||
|
'animate__animated animate__fadeIn': !showRecovery && !isInitialLoad}">
|
||||||
<div class="login-card shadow-2 border-round">
|
<div class="login-card shadow-2 border-round">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h2>Iniciar Sesión</h2>
|
<h2>Iniciar Sesión</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-3">
|
<form (ngSubmit)="onLogin()" class="p-3">
|
||||||
<!-- Mensaje de información -->
|
<!-- Email -->
|
||||||
<div class="info-message mb-4 text-center">
|
<div class="field mb-3">
|
||||||
<p>Haz clic en el botón para iniciar sesión con la cuenta de usuario registrada en el sistema.</p>
|
<input type="email" pInputText [(ngModel)]="email" name="email" placeholder="Email"
|
||||||
|
class="input-with-icon w-full" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Botón para iniciar sesión con Keycloak -->
|
<!-- Password -->
|
||||||
|
<div class="field mb-3">
|
||||||
|
<input type="password" pInputText [(ngModel)]="password" name="password" placeholder="Password"
|
||||||
|
class="input-with-lock w-full" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje de error -->
|
||||||
|
<div *ngIf="errorMessage" class="error-message my-2">
|
||||||
|
<p-message severity="error" [text]="errorMessage"></p-message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón -->
|
||||||
<div class="login-actions">
|
<div class="login-actions">
|
||||||
<button pButton type="button" (click)="onLogin()"
|
<button pButton type="submit" [label]="loading ? 'Autenticando...' : 'Autenticar'"
|
||||||
[label]="loading ? 'Redirigiendo...' : 'Iniciar Sesión'"
|
class="p-button-primary w-full" [disabled]="loading || !email || !password">
|
||||||
icon="pi pi-sign-in"
|
|
||||||
class="p-button-primary w-full"
|
|
||||||
[disabled]="loading">
|
|
||||||
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
|
<i *ngIf="loading" class="pi pi-spin pi-spinner mr-2"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Recuperar contraseña -->
|
||||||
|
<div class="password-recovery px-3 pb-3">
|
||||||
|
<a href="#" (click)="toggleRecovery($event)">Recuperar Contraseña</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Información adicional (opcional) -->
|
<!-- Credenciales de prueba -->
|
||||||
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
|
<div class="test-credentials mx-3 mb-3 p-2 border-round bg-gray-100">
|
||||||
<p class="mb-1 font-bold">Información:</p>
|
<p class="mb-1 font-bold">Credenciales de prueba:</p>
|
||||||
<p class="mb-1">Serás redirigido al sistema de autenticación.</p>
|
<p class="mb-1">Email: admin@example.com</p>
|
||||||
<p>Si tienes problemas, contacta al administrador.</p>
|
<p>Password: admin123</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PANEL DE RECUPERACIÓN -->
|
||||||
|
<div class="panel-container w-full"
|
||||||
|
[ngClass]="{'animate__animated animate__fadeIn': showRecovery,
|
||||||
|
'animate__animated animate__fadeOut d-none': !showRecovery && !isInitialLoad}">
|
||||||
|
<div class="login-card shadow-2 border-round">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>Recuperar Contraseña</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form (ngSubmit)="onRecoverPassword()" class="p-3">
|
||||||
|
<!-- Email para recuperación -->
|
||||||
|
<div class="field mb-3">
|
||||||
|
<input type="email" pInputText [(ngModel)]="recoveryEmail" name="recoveryEmail"
|
||||||
|
placeholder="Ingresa tu email" class="input-with-icon w-full" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje informativo -->
|
||||||
|
<div class="info-message mb-3">
|
||||||
|
<p class="text-sm">Te enviaremos un enlace para restablecer tu contraseña.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mensaje de estado de recuperación -->
|
||||||
|
<div *ngIf="recoveryMessage" class="recovery-message my-2">
|
||||||
|
<p-message [severity]="recoveryStatus" [text]="recoveryMessage"></p-message>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de enviar -->
|
||||||
|
<div class="login-actions">
|
||||||
|
<button pButton type="submit" [label]="recoveryLoading ? 'Enviando...' : 'Enviar Enlace'"
|
||||||
|
class="p-button-primary w-full" [disabled]="recoveryLoading || !recoveryEmail">
|
||||||
|
<i *ngIf="recoveryLoading" class="pi pi-spin pi-spinner mr-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Volver al login -->
|
||||||
|
<div class="password-recovery px-3 pb-3">
|
||||||
|
<a href="#" (click)="toggleRecovery($event)">Volver al Login</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { CardModule } from 'primeng/card';
|
import { CardModule } from 'primeng/card';
|
||||||
import { InputTextModule } from 'primeng/inputtext';
|
import { InputTextModule } from 'primeng/inputtext';
|
||||||
import { ButtonModule } from 'primeng/button';
|
import { ButtonModule } from 'primeng/button';
|
||||||
@ -12,14 +13,12 @@ import { ToastModule } from 'primeng/toast';
|
|||||||
import { MessageService } from 'primeng/api';
|
import { MessageService } from 'primeng/api';
|
||||||
import { FooterComponent } from "../../components/footer/footer.component";
|
import { FooterComponent } from "../../components/footer/footer.component";
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
standalone: true,
|
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
CardModule,
|
CardModule,
|
||||||
InputTextModule,
|
InputTextModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
@ -35,76 +34,103 @@ import { firstValueFrom } from 'rxjs';
|
|||||||
styleUrl: './login.component.scss'
|
styleUrl: './login.component.scss'
|
||||||
})
|
})
|
||||||
export class LoginComponent implements OnInit {
|
export class LoginComponent implements OnInit {
|
||||||
// Inyectar el servicio de autenticación
|
// Login form
|
||||||
private authService = inject(AuthService);
|
email: string = '';
|
||||||
private route = inject(ActivatedRoute);
|
password: string = '';
|
||||||
private router = inject(Router);
|
|
||||||
|
|
||||||
loading: boolean = false;
|
loading: boolean = false;
|
||||||
returnUrl: string = '';
|
errorMessage: string = '';
|
||||||
|
|
||||||
constructor(private messageService: MessageService) {}
|
// Password recovery form
|
||||||
|
recoveryEmail: string = '';
|
||||||
|
recoveryLoading: boolean = false;
|
||||||
|
recoveryMessage: string = '';
|
||||||
|
recoveryStatus: string = 'info';
|
||||||
|
|
||||||
|
// Control de formularios
|
||||||
|
showRecovery: boolean = false;
|
||||||
|
isInitialLoad: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private authService: AuthService,
|
||||||
|
private messageService: MessageService
|
||||||
|
) { }
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
// Obtener el returnUrl de los query params si existe
|
|
||||||
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/inicio';
|
|
||||||
|
|
||||||
// Simplificación - verificar autenticación sin promesas anidadas
|
|
||||||
this.authService.isLoggedIn().subscribe({
|
|
||||||
next: (isLoggedIn) => {
|
|
||||||
if (isLoggedIn) {
|
|
||||||
console.log('Usuario ya autenticado, redirigiendo a:', this.returnUrl);
|
|
||||||
// Verificar si la URL de retorno es válida
|
|
||||||
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
|
|
||||||
// Redirigir
|
|
||||||
this.router.navigate([effectiveReturnUrl], { replaceUrl: true });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (error) => console.error('Error al verificar autenticación:', error)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Método de login con Keycloak usando nuestro servicio AuthService
|
/**
|
||||||
async onLogin() {
|
* Cambia entre el formulario de login y recuperación
|
||||||
this.loading = true;
|
*/
|
||||||
|
|
||||||
try {
|
|
||||||
// Verificar si la URL de retorno es válida
|
|
||||||
const effectiveReturnUrl = this.returnUrl === '/' ? '/inicio' : this.returnUrl;
|
|
||||||
|
|
||||||
// Construir el redirectUri
|
|
||||||
const redirectUri = window.location.origin + effectiveReturnUrl;
|
|
||||||
console.log('Iniciando login con redirectUri:', redirectUri);
|
|
||||||
|
|
||||||
// Iniciar el flujo de autenticación de Keycloak
|
|
||||||
await this.authService.login(redirectUri);
|
|
||||||
// Keycloak se encargará de la redirección
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al iniciar sesión:', error);
|
|
||||||
this.loading = false;
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'Error al intentar iniciar sesión. Por favor, inténtelo de nuevo.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mantenemos los métodos de recuperación de contraseña para compatibilidad
|
|
||||||
toggleRecovery(event: Event) {
|
toggleRecovery(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.messageService.add({
|
this.showRecovery = !this.showRecovery;
|
||||||
severity: 'info',
|
this.errorMessage = '';
|
||||||
summary: 'Información',
|
this.recoveryMessage = '';
|
||||||
detail: 'Para recuperar tu contraseña, utiliza la opción en la pantalla de login de Keycloak'
|
|
||||||
});
|
// Si estamos cambiando al formulario de recuperación, copiar el email actual
|
||||||
|
if (this.showRecovery && this.email) {
|
||||||
|
this.recoveryEmail = this.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forzar actualización del DOM con un pequeño retraso
|
||||||
|
setTimeout(() => {
|
||||||
|
// Este timeout ayuda a que Angular aplique los cambios de clase completamente
|
||||||
|
}, 50); // Aumentado para asegurar la transición suave
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceso de login
|
||||||
|
*/
|
||||||
|
onLogin() {
|
||||||
|
this.loading = true;
|
||||||
|
this.errorMessage = '';
|
||||||
|
|
||||||
|
// Llamar al servicio auth
|
||||||
|
this.authService.login({ email: this.email, password: this.password })
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
console.log('Login exitoso');
|
||||||
|
this.loading = false;
|
||||||
|
this.router.navigate(['/inicio']);
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
console.error('Error en login:', error);
|
||||||
|
this.loading = false;
|
||||||
|
this.errorMessage = 'Credenciales incorrectas';
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'Credenciales incorrectas'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proceso de recuperación de contraseña
|
||||||
|
*/
|
||||||
onRecoverPassword() {
|
onRecoverPassword() {
|
||||||
this.messageService.add({
|
if (!this.recoveryEmail) {
|
||||||
severity: 'info',
|
this.recoveryMessage = 'Debes ingresar un email';
|
||||||
summary: 'Información',
|
this.recoveryStatus = 'error';
|
||||||
detail: 'Esta funcionalidad ahora es manejada por Keycloak'
|
return;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
this.recoveryLoading = true;
|
||||||
|
this.recoveryMessage = '';
|
||||||
|
|
||||||
|
// Simulamos la recuperación (en producción, esto llamaría a un servicio real)
|
||||||
|
setTimeout(() => {
|
||||||
|
this.recoveryLoading = false;
|
||||||
|
this.recoveryMessage = 'Hemos enviado un enlace de recuperación a tu email';
|
||||||
|
this.recoveryStatus = 'success';
|
||||||
|
this.messageService.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Email enviado',
|
||||||
|
detail: 'Se ha enviado un enlace de recuperación a tu correo'
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,240 +1,111 @@
|
|||||||
import { Injectable, inject, effect, signal, computed } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { BehaviorSubject, Observable, from } from 'rxjs';
|
import { Observable, BehaviorSubject, of, throwError } from 'rxjs';
|
||||||
import { KEYCLOAK_EVENT_SIGNAL, KeycloakEventType } from 'keycloak-angular';
|
import { tap, delay } from 'rxjs/operators';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import Keycloak from 'keycloak-js';
|
interface LoginCredentials {
|
||||||
import { MessageService } from 'primeng/api';
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuthResponse {
|
||||||
|
token: string;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
email?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
// Inject Keycloak instance directly
|
private apiUrl = 'api/auth';
|
||||||
private keycloak = inject(Keycloak);
|
|
||||||
private keycloakEvents = inject(KEYCLOAK_EVENT_SIGNAL);
|
|
||||||
private router = inject(Router);
|
|
||||||
private messageService = inject(MessageService);
|
|
||||||
|
|
||||||
// User state
|
|
||||||
private userSubject = new BehaviorSubject<any>(null);
|
private userSubject = new BehaviorSubject<any>(null);
|
||||||
public user$ = this.userSubject.asObservable();
|
public user$ = this.userSubject.asObservable();
|
||||||
|
|
||||||
// Authentication state as a signal
|
// Usuarios de prueba para simular login
|
||||||
public isAuthenticated = signal<boolean>(false);
|
private mockUsers = [
|
||||||
|
{
|
||||||
// Login error state
|
email: 'admin@example.com',
|
||||||
public loginError = signal<string | null>(null);
|
password: 'admin123',
|
||||||
|
user: {
|
||||||
|
id: 1,
|
||||||
|
username: 'admin',
|
||||||
|
name: 'Administrador',
|
||||||
|
role: 'admin',
|
||||||
|
email: 'admin@example.com'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: 'user@example.com',
|
||||||
|
password: 'user123',
|
||||||
|
user: {
|
||||||
|
id: 2,
|
||||||
|
username: 'user',
|
||||||
|
name: 'Usuario',
|
||||||
|
role: 'user',
|
||||||
|
email: 'user@example.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
// Check initial state
|
// Check if user is already logged in on service initialization
|
||||||
this.checkInitialAuthState();
|
const user = localStorage.getItem('user');
|
||||||
|
if (user) {
|
||||||
// Set up event handlers using Angular effects
|
this.userSubject.next(JSON.parse(user));
|
||||||
effect(() => {
|
|
||||||
const event = this.keycloakEvents();
|
|
||||||
if (!event) return;
|
|
||||||
|
|
||||||
console.log('Keycloak event:', event.type);
|
|
||||||
|
|
||||||
// On successful login
|
|
||||||
if (event.type === KeycloakEventType.AuthSuccess) {
|
|
||||||
this.isAuthenticated.set(true);
|
|
||||||
this.loginError.set(null);
|
|
||||||
this.loadUserInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On logout
|
|
||||||
if (event.type === KeycloakEventType.AuthLogout) {
|
|
||||||
this.isAuthenticated.set(false);
|
|
||||||
this.userSubject.next(null);
|
|
||||||
this.router.navigate(['/login']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// On authentication error
|
|
||||||
if (event.type === KeycloakEventType.AuthError) {
|
|
||||||
console.error('Authentication error:', event);
|
|
||||||
this.isAuthenticated.set(false);
|
|
||||||
this.userSubject.next(null);
|
|
||||||
|
|
||||||
// Mostrar mensaje de error
|
|
||||||
const errorMsg = 'Error de autenticación. Por favor, verifica tus credenciales o inténtalo más tarde.';
|
|
||||||
this.loginError.set(errorMsg);
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error de autenticación',
|
|
||||||
detail: errorMsg,
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// On token expiration
|
|
||||||
if (event.type === KeycloakEventType.TokenExpired) {
|
|
||||||
console.log('Token expired, refreshing...');
|
|
||||||
this.updateToken();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkInitialAuthState(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const isLoggedIn = await this.keycloak.authenticated;
|
|
||||||
this.isAuthenticated.set(isLoggedIn);
|
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
await this.loadUserInfo();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking initial auth state:', error);
|
|
||||||
this.isAuthenticated.set(false);
|
|
||||||
|
|
||||||
// Mostrar mensaje de error
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'No se pudo verificar el estado de autenticación.',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadUserInfo(): Promise<void> {
|
login(credentials: LoginCredentials): Observable<AuthResponse> {
|
||||||
try {
|
// Simular login con usuarios de prueba
|
||||||
const isLoggedIn = await this.keycloak.authenticated;
|
const user = this.mockUsers.find(u =>
|
||||||
|
u.email === credentials.email && u.password === credentials.password);
|
||||||
if (!isLoggedIn) {
|
|
||||||
this.userSubject.next(null);
|
if (user) {
|
||||||
return;
|
// Generar token falso
|
||||||
}
|
const mockResponse: AuthResponse = {
|
||||||
|
token: 'mock-jwt-token-' + Math.random().toString(36).substring(2, 15),
|
||||||
const userProfile = await this.keycloak.loadUserProfile();
|
user: user.user
|
||||||
const isAdmin = this.keycloak.hasRealmRole('admin');
|
|
||||||
|
|
||||||
// Get user roles
|
|
||||||
const realmRoles = this.keycloak.realmAccess?.roles || [];
|
|
||||||
const resourceRoles = this.keycloak.resourceAccess || {};
|
|
||||||
|
|
||||||
const user = {
|
|
||||||
id: userProfile.id,
|
|
||||||
username: userProfile.username,
|
|
||||||
name: `${userProfile.firstName || ''} ${userProfile.lastName || ''}`.trim(),
|
|
||||||
email: userProfile.email,
|
|
||||||
role: isAdmin ? 'admin' : 'user',
|
|
||||||
roles: {
|
|
||||||
realm: realmRoles,
|
|
||||||
resource: resourceRoles
|
|
||||||
},
|
|
||||||
isAdmin: isAdmin
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.userSubject.next(user);
|
return of(mockResponse).pipe(
|
||||||
} catch (error) {
|
// Simular retraso de red
|
||||||
console.error('Error loading user profile:', error);
|
delay(800),
|
||||||
this.userSubject.next(null);
|
tap(response => {
|
||||||
|
// Store token and user info
|
||||||
this.messageService.add({
|
localStorage.setItem('token', response.token);
|
||||||
severity: 'error',
|
localStorage.setItem('user', JSON.stringify(response.user));
|
||||||
summary: 'Error',
|
this.userSubject.next(response.user);
|
||||||
detail: 'No se pudo cargar la información del usuario.',
|
})
|
||||||
life: 5000
|
);
|
||||||
});
|
} else {
|
||||||
|
// Simular error de credenciales inválidas
|
||||||
|
return throwError(() => new Error('Credenciales incorrectas'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(redirectUri?: string): Promise<void> {
|
logout(): void {
|
||||||
try {
|
// Clear storage and update subject
|
||||||
this.loginError.set(null);
|
localStorage.removeItem('token');
|
||||||
await this.keycloak.login({
|
localStorage.removeItem('user');
|
||||||
redirectUri: redirectUri || window.location.origin
|
this.userSubject.next(null);
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login error:', error);
|
|
||||||
this.loginError.set('Error al iniciar sesión. Por favor, inténtalo de nuevo.');
|
|
||||||
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error de inicio de sesión',
|
|
||||||
detail: 'No se pudo iniciar sesión. Por favor, inténtalo de nuevo.',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
isLoggedIn(): boolean {
|
||||||
try {
|
return !!localStorage.getItem('token');
|
||||||
await this.keycloak.logout({
|
|
||||||
redirectUri: window.location.origin
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Logout error:', error);
|
|
||||||
// Intento manual de navegar a login en caso de error
|
|
||||||
this.router.navigate(['/login']);
|
|
||||||
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'error',
|
|
||||||
summary: 'Error',
|
|
||||||
detail: 'Error al cerrar sesión.',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoggedIn(): Observable<boolean> {
|
getToken(): string | null {
|
||||||
try {
|
return localStorage.getItem('token');
|
||||||
// Usar directamente la propiedad authenticated de Keycloak
|
|
||||||
return from(Promise.resolve(this.keycloak.authenticated || false));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al verificar autenticación:', error);
|
|
||||||
return from(Promise.resolve(false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getToken(): Promise<string> {
|
|
||||||
try {
|
|
||||||
return Promise.resolve(this.keycloak.token || '');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error al obtener token:', error);
|
|
||||||
return Promise.resolve('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateToken(minValidity = 30): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
return await this.keycloak.updateToken(minValidity);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error refreshing token:', error);
|
|
||||||
// No redireccionar automáticamente al login, mostrar mensaje primero
|
|
||||||
this.messageService.add({
|
|
||||||
severity: 'warn',
|
|
||||||
summary: 'Sesión expirada',
|
|
||||||
detail: 'Tu sesión ha expirado. Por favor, inicia sesión nuevamente.',
|
|
||||||
life: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
// Esperar un momento para que el usuario vea el mensaje
|
|
||||||
setTimeout(() => this.login(), 2000);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentUser(): any {
|
getCurrentUser(): any {
|
||||||
return this.userSubject.value;
|
return this.userSubject.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has a specific role
|
|
||||||
hasRole(role: string): boolean {
|
|
||||||
return this.keycloak.hasRealmRole(role);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user has any of the specified roles
|
|
||||||
hasAnyRole(roles: string[]): boolean {
|
|
||||||
for (const role of roles) {
|
|
||||||
if (this.keycloak.hasRealmRole(role)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>correo</title>
|
<title>cronogramas</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
|
|||||||
@ -1,963 +0,0 @@
|
|||||||
# Tutorial: Implementación de Keycloak con LDAP para Angular
|
|
||||||
|
|
||||||
Este tutorial explica cómo configurar un sistema de autenticación completo utilizando Keycloak como proveedor de identidad, OpenLDAP como directorio de usuarios y una aplicación Angular que consume estos servicios.
|
|
||||||
|
|
||||||
## Índice
|
|
||||||
|
|
||||||
1. [Preparación del entorno Ubuntu](#1-preparaci%C3%B3n-del-entorno-ubuntu)
|
|
||||||
2. [Instalación y configuración de OpenLDAP](#2-instalaci%C3%B3n-y-configuraci%C3%B3n-de-openldap)
|
|
||||||
3. [Crear estructura LDAP para correos.com](#3-crear-estructura-ldap-para-correoscom)
|
|
||||||
4. [Instalación y configuración de Keycloak](#4-instalaci%C3%B3n-y-configuraci%C3%B3n-de-keycloak)
|
|
||||||
5. [Configuración de Keycloak en la interfaz web](#5-configuraci%C3%B3n-de-keycloak-en-la-interfaz-web)
|
|
||||||
6. [Configuración de una aplicación Angular](#6-configuraci%C3%B3n-de-una-aplicaci%C3%B3n-angular)
|
|
||||||
7. [Arquitectura del sistema](#7-arquitectura-del-sistema)
|
|
||||||
8. [Resolución de problemas comunes](#8-resoluci%C3%B3n-de-problemas-comunes)
|
|
||||||
9. [Verificación y prueba del sistema](#9-verificaci%C3%B3n-y-prueba-del-sistema)
|
|
||||||
10. [Resumen](#10-resumen)
|
|
||||||
|
|
||||||
## 1. Preparación del entorno Ubuntu
|
|
||||||
|
|
||||||
Primero, actualizamos el sistema e instalamos Java:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt upgrade -y
|
|
||||||
sudo apt install openjdk-17-jdk -y
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Verifica la instalación de Java:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
java -version
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2. Instalación y configuración de OpenLDAP
|
|
||||||
|
|
||||||
### Instalar OpenLDAP y utilidades
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install slapd ldap-utils -y
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Durante la instalación, se te pedirá configurar una contraseña de administrador para LDAP.
|
|
||||||
|
|
||||||
### Reconfigurar LDAP con el dominio correcto
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dpkg-reconfigure slapd
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
En la configuración:
|
|
||||||
|
|
||||||
1. "¿Omitir configuración del servidor LDAP?" → No
|
|
||||||
2. "Nombre de dominio DNS:" → **correos.com**
|
|
||||||
3. "Nombre de la organización:" → Correos Org
|
|
||||||
4. "Contraseña de administrador:" → [tu contraseña segura]
|
|
||||||
5. "Confirmar contraseña:" → [repetir la contraseña]
|
|
||||||
6. "Motor de base de datos:" → MDB
|
|
||||||
7. "¿Quiere que se elimine la base de datos cuando se purgue slapd?" → No
|
|
||||||
8. "¿Mover la base de datos antigua?" → Sí
|
|
||||||
|
|
||||||
### Verificar que LDAP se esté ejecutando correctamente
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl status slapd
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comprobar la conexión LDAP básica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapsearch -x -H ldap://localhost -b dc=correos,dc=com -D "cn=admin,dc=correos,dc=com" -W
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Instalar phpLDAPadmin para la gestión gráfica
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install phpldapadmin -y
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurar phpLDAPadmin
|
|
||||||
|
|
||||||
Edita el archivo de configuración:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/phpldapadmin/config.php
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Busca y modifica las siguientes líneas:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$servers->setValue('server','host','127.0.0.1');
|
|
||||||
$servers->setValue('server','base',array('dc=correos,dc=com'));
|
|
||||||
$servers->setValue('login','bind_id','cn=admin,dc=correos,dc=com');
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Y cambia esta línea:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$servers->setValue('login','anon_bind',true);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
por:
|
|
||||||
|
|
||||||
```php
|
|
||||||
$servers->setValue('login','anon_bind',false);
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Reinicia el servidor web:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl restart apache2
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Crear estructura LDAP para correos.com
|
|
||||||
|
|
||||||
### Crear unidades organizativas
|
|
||||||
|
|
||||||
Crea un archivo para las unidades organizativas:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano ~/ou.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Con el siguiente contenido:
|
|
||||||
|
|
||||||
```ldif
|
|
||||||
dn: ou=grupos,dc=correos,dc=com
|
|
||||||
objectClass: organizationalUnit
|
|
||||||
ou: grupos
|
|
||||||
|
|
||||||
dn: ou=usuarios,dc=correos,dc=com
|
|
||||||
objectClass: organizationalUnit
|
|
||||||
ou: usuarios
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Aplica los cambios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/ou.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear grupos LDAP
|
|
||||||
|
|
||||||
Crea un archivo para los grupos:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano ~/grupos.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Con el siguiente contenido:
|
|
||||||
|
|
||||||
```ldif
|
|
||||||
dn: cn=administradores,ou=grupos,dc=correos,dc=com
|
|
||||||
objectClass: posixGroup
|
|
||||||
cn: administradores
|
|
||||||
gidNumber: 1000
|
|
||||||
|
|
||||||
dn: cn=desarrolladores,ou=grupos,dc=correos,dc=com
|
|
||||||
objectClass: posixGroup
|
|
||||||
cn: desarrolladores
|
|
||||||
gidNumber: 1001
|
|
||||||
|
|
||||||
dn: cn=usuarios,ou=grupos,dc=correos,dc=com
|
|
||||||
objectClass: posixGroup
|
|
||||||
cn: usuarios
|
|
||||||
gidNumber: 1002
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Aplica los cambios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/grupos.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear usuarios LDAP
|
|
||||||
|
|
||||||
Primero, genera contraseñas encriptadas para los usuarios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
slappasswd -s "password123"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Anota el hash resultante para usarlo en el siguiente archivo.
|
|
||||||
|
|
||||||
Crea un archivo para los usuarios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano ~/usuarios.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Con el siguiente contenido (reemplazando {HASH} con el hash que generaste):
|
|
||||||
|
|
||||||
```ldif
|
|
||||||
dn: uid=admin,ou=usuarios,dc=correos,dc=com
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
objectClass: posixAccount
|
|
||||||
objectClass: shadowAccount
|
|
||||||
uid: admin
|
|
||||||
sn: Admin
|
|
||||||
givenName: Admin
|
|
||||||
cn: Admin User
|
|
||||||
displayName: Admin User
|
|
||||||
uidNumber: 1000
|
|
||||||
gidNumber: 1000
|
|
||||||
userPassword: {HASH}
|
|
||||||
loginShell: /bin/bash
|
|
||||||
homeDirectory: /home/admin
|
|
||||||
mail: admin@correos.com
|
|
||||||
|
|
||||||
dn: uid=developer,ou=usuarios,dc=correos,dc=com
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
objectClass: posixAccount
|
|
||||||
objectClass: shadowAccount
|
|
||||||
uid: developer
|
|
||||||
sn: Developer
|
|
||||||
givenName: Dev
|
|
||||||
cn: Dev User
|
|
||||||
displayName: Developer User
|
|
||||||
uidNumber: 1001
|
|
||||||
gidNumber: 1001
|
|
||||||
userPassword: {HASH}
|
|
||||||
loginShell: /bin/bash
|
|
||||||
homeDirectory: /home/developer
|
|
||||||
mail: developer@correos.com
|
|
||||||
|
|
||||||
dn: uid=user,ou=usuarios,dc=correos,dc=com
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
objectClass: posixAccount
|
|
||||||
objectClass: shadowAccount
|
|
||||||
uid: user
|
|
||||||
sn: User
|
|
||||||
givenName: Normal
|
|
||||||
cn: Normal User
|
|
||||||
displayName: Normal User
|
|
||||||
uidNumber: 1002
|
|
||||||
gidNumber: 1002
|
|
||||||
userPassword: {HASH}
|
|
||||||
loginShell: /bin/bash
|
|
||||||
homeDirectory: /home/user
|
|
||||||
mail: user@correos.com
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Aplica los cambios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapadd -x -D cn=admin,dc=correos,dc=com -W -f ~/usuarios.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Asociar usuarios a grupos
|
|
||||||
|
|
||||||
Crea un archivo para las membresías:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano ~/miembros.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Con el siguiente contenido:
|
|
||||||
|
|
||||||
```ldif
|
|
||||||
dn: cn=administradores,ou=grupos,dc=correos,dc=com
|
|
||||||
changetype: modify
|
|
||||||
add: memberUid
|
|
||||||
memberUid: admin
|
|
||||||
|
|
||||||
dn: cn=desarrolladores,ou=grupos,dc=correos,dc=com
|
|
||||||
changetype: modify
|
|
||||||
add: memberUid
|
|
||||||
memberUid: developer
|
|
||||||
|
|
||||||
dn: cn=usuarios,ou=grupos,dc=correos,dc=com
|
|
||||||
changetype: modify
|
|
||||||
add: memberUid
|
|
||||||
memberUid: user
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Aplica los cambios:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapmodify -x -D cn=admin,dc=correos,dc=com -W -f ~/miembros.ldif
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verificar la estructura LDAP
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ldapsearch -x -H ldap://localhost -b dc=correos,dc=com -D "cn=admin,dc=correos,dc=com" -W
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Instalación y configuración de Keycloak
|
|
||||||
|
|
||||||
### Descargar e instalar Keycloak
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Crear directorio para Keycloak
|
|
||||||
mkdir -p ~/keycloak
|
|
||||||
cd ~/keycloak
|
|
||||||
|
|
||||||
# Descargar la última versión de Keycloak
|
|
||||||
wget https://github.com/keycloak/keycloak/releases/download/21.1.1/keycloak-21.1.1.tar.gz
|
|
||||||
|
|
||||||
# Extraer el archivo
|
|
||||||
tar -xvzf keycloak-21.1.1.tar.gz
|
|
||||||
|
|
||||||
# Mover a una ubicación más adecuada
|
|
||||||
sudo mv keycloak-21.1.1 /opt/keycloak
|
|
||||||
|
|
||||||
# Crear un usuario para Keycloak
|
|
||||||
sudo useradd -r -s /sbin/nologin keycloak
|
|
||||||
|
|
||||||
# Asignar permisos
|
|
||||||
sudo chown -R keycloak:keycloak /opt/keycloak
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurar usuario administrador inicial para Keycloak
|
|
||||||
|
|
||||||
Crea un archivo de propiedades:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /opt/keycloak/conf/keycloak.conf
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Agrega estas líneas:
|
|
||||||
|
|
||||||
```
|
|
||||||
# Configuración básica
|
|
||||||
http-port=8080
|
|
||||||
https-port=8443
|
|
||||||
hostname=localhost
|
|
||||||
|
|
||||||
# Configuración de administrador inicial
|
|
||||||
http-enabled=true
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Iniciar Keycloak en modo desarrollo
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /opt/keycloak
|
|
||||||
export KEYCLOAK_ADMIN=admin
|
|
||||||
export KEYCLOAK_ADMIN_PASSWORD=admin
|
|
||||||
sudo -u keycloak bin/kc.sh start-dev
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admin".
|
|
||||||
|
|
||||||
### Configurar Keycloak como servicio
|
|
||||||
|
|
||||||
En otra terminal, crea un archivo de servicio systemd:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/systemd/system/keycloak.service
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Con el siguiente contenido:
|
|
||||||
|
|
||||||
```ini
|
|
||||||
[Unit]
|
|
||||||
Description=Keycloak Application Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=idle
|
|
||||||
User=keycloak
|
|
||||||
Group=keycloak
|
|
||||||
Environment="KEYCLOAK_ADMIN=admin"
|
|
||||||
Environment="KEYCLOAK_ADMIN_PASSWORD=admin"
|
|
||||||
ExecStart=/opt/keycloak/bin/kc.sh start-dev
|
|
||||||
TimeoutStartSec=600
|
|
||||||
TimeoutStopSec=600
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
En el futuro, podrás habilitar e iniciar el servicio con:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo systemctl daemon-reload
|
|
||||||
sudo systemctl enable keycloak
|
|
||||||
sudo systemctl start keycloak
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Configuración de Keycloak en la interfaz web
|
|
||||||
|
|
||||||
Ahora puedes acceder a la consola de administración de Keycloak en http://localhost:8080/admin/ e iniciar sesión con:
|
|
||||||
|
|
||||||
- Usuario: `admin`
|
|
||||||
- Contraseña: `admin`
|
|
||||||
|
|
||||||
### Crear un nuevo Reino (Realm)
|
|
||||||
|
|
||||||
1. Haz clic en el menú desplegable superior izquierdo que dice "master"
|
|
||||||
2. Selecciona "Create Realm"
|
|
||||||
3. Ingresa el nombre: `angular-app`
|
|
||||||
4. Haz clic en "Create"
|
|
||||||
|
|
||||||
### Configurar la Federación de Usuarios LDAP
|
|
||||||
|
|
||||||
1. En el menú lateral izquierdo, selecciona "User Federation"
|
|
||||||
|
|
||||||
2. Haz clic en "Add provider" → "ldap"
|
|
||||||
|
|
||||||
3. Completa los siguientes campos:
|
|
||||||
|
|
||||||
- Console Display Name: `LDAP`
|
|
||||||
- Vendor: `Other`
|
|
||||||
- Connection URL: `ldap://localhost:389`
|
|
||||||
- Enable StartTLS: `OFF`
|
|
||||||
- Bind Type: `simple`
|
|
||||||
- Bind DN: `cn=admin,dc=correos,dc=com`
|
|
||||||
- Bind Credential: (la contraseña de admin LDAP)
|
|
||||||
- Edit Mode: `WRITABLE`
|
|
||||||
- Users DN: `ou=usuarios,dc=correos,dc=com`
|
|
||||||
- Username LDAP attribute: `uid`
|
|
||||||
- RDN LDAP attribute: `uid`
|
|
||||||
- UUID LDAP attribute: `entryUUID`
|
|
||||||
- User Object Classes: `inetOrgPerson, posixAccount`
|
|
||||||
- Custom User LDAP Filter: (dejar en blanco)
|
|
||||||
- Search Scope: `One Level`
|
|
||||||
4. Haz clic en "Test connection" y "Test authentication" para verificar
|
|
||||||
|
|
||||||
5. Guarda la configuración
|
|
||||||
|
|
||||||
6. En la pantalla del proveedor LDAP, ve a la pestaña "Synchronization"
|
|
||||||
|
|
||||||
7. Haz clic en "Sync all users"
|
|
||||||
|
|
||||||
|
|
||||||
### Configurar el Mapeo de Grupos LDAP
|
|
||||||
|
|
||||||
1. En la pantalla del proveedor LDAP, ve a la pestaña "Mappers"
|
|
||||||
2. Haz clic en "Create"
|
|
||||||
3. Completa:
|
|
||||||
- Name: `group-mapper`
|
|
||||||
- Mapper Type: `group-ldap-mapper`
|
|
||||||
- LDAP Groups DN: `ou=grupos,dc=correos,dc=com`
|
|
||||||
- Group Object Classes: `posixGroup`
|
|
||||||
- Membership LDAP Attribute: `memberUid`
|
|
||||||
- Group Name LDAP Attribute: `cn`
|
|
||||||
- User Roles Retrieve Strategy: `LOAD_GROUPS_BY_MEMBER_ATTRIBUTE`
|
|
||||||
- Member-Of LDAP Attribute: `memberOf`
|
|
||||||
- Mapped Group Attributes: (dejar en blanco)
|
|
||||||
- Drop non-existing groups during sync: `ON`
|
|
||||||
4. Haz clic en "Save"
|
|
||||||
5. En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak"
|
|
||||||
|
|
||||||
### Crear un Cliente para Angular
|
|
||||||
|
|
||||||
1. En el menú lateral izquierdo, selecciona "Clients"
|
|
||||||
|
|
||||||
2. Haz clic en "Create client"
|
|
||||||
|
|
||||||
3. Completa:
|
|
||||||
|
|
||||||
- Client type: `OpenID Connect`
|
|
||||||
- Client ID: `angular-app`
|
|
||||||
- Name: `Angular Application`
|
|
||||||
- Haz clic en "Next"
|
|
||||||
4. En la siguiente pantalla:
|
|
||||||
|
|
||||||
- Client authentication: `ON`
|
|
||||||
- Authorization: `OFF`
|
|
||||||
- Haz clic en "Next"
|
|
||||||
5. En la siguiente pantalla:
|
|
||||||
|
|
||||||
- Root URL: `http://localhost:4200`
|
|
||||||
- Home URL: `/`
|
|
||||||
- Valid redirect URIs: `http://localhost:4200/*`
|
|
||||||
- Web origins: `http://localhost:4200`
|
|
||||||
- Haz clic en "Save"
|
|
||||||
6. En la pestaña "Credentials" del cliente, copia el "Client secret" (lo necesitarás para la aplicación Angular)
|
|
||||||
|
|
||||||
|
|
||||||
## 6. Configuración de una aplicación Angular
|
|
||||||
|
|
||||||
Primero, instala Node.js y npm:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
|
|
||||||
sudo apt-get install -y nodejs
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear una aplicación Angular
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Instalar Angular CLI
|
|
||||||
npm install -g @angular/cli
|
|
||||||
|
|
||||||
# Crear una nueva aplicación
|
|
||||||
ng new angular-keycloak-app
|
|
||||||
cd angular-keycloak-app
|
|
||||||
|
|
||||||
# Instalar la biblioteca para integrar Keycloak
|
|
||||||
npm install keycloak-angular keycloak-js
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurar Keycloak en Angular
|
|
||||||
|
|
||||||
Crea un archivo de configuración en `src/assets/keycloak.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"realm": "angular-app",
|
|
||||||
"auth-server-url": "http://localhost:8080/",
|
|
||||||
"resource": "angular-app",
|
|
||||||
"credentials": {
|
|
||||||
"secret": "TU_CLIENT_SECRET_AQUÍ"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Modifica el archivo `src/app/app.module.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NgModule, APP_INITIALIZER } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { KeycloakAngularModule, KeycloakService } from 'keycloak-angular';
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
function initializeKeycloak(keycloak: KeycloakService) {
|
|
||||||
return () =>
|
|
||||||
keycloak.init({
|
|
||||||
config: {
|
|
||||||
url: 'http://localhost:8080',
|
|
||||||
realm: 'angular-app',
|
|
||||||
clientId: 'angular-app'
|
|
||||||
},
|
|
||||||
initOptions: {
|
|
||||||
onLoad: 'check-sso',
|
|
||||||
silentCheckSsoRedirectUri:
|
|
||||||
window.location.origin + '/assets/silent-check-sso.html'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
AppRoutingModule,
|
|
||||||
KeycloakAngularModule
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: APP_INITIALIZER,
|
|
||||||
useFactory: initializeKeycloak,
|
|
||||||
multi: true,
|
|
||||||
deps: [KeycloakService]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
bootstrap: [AppComponent]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Crea un archivo `src/assets/silent-check-sso.html`:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<html>
|
|
||||||
<body>
|
|
||||||
<script>
|
|
||||||
parent.postMessage(location.href, location.origin);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear un servicio de autenticación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ng generate service auth
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Edita `src/app/auth.service.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
|
||||||
import { KeycloakProfile } from 'keycloak-js';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthService {
|
|
||||||
|
|
||||||
constructor(private keycloak: KeycloakService) { }
|
|
||||||
|
|
||||||
public getLoggedUser(): Promise<KeycloakProfile | null> {
|
|
||||||
return this.keycloak.loadUserProfile();
|
|
||||||
}
|
|
||||||
|
|
||||||
public login(): void {
|
|
||||||
this.keycloak.login();
|
|
||||||
}
|
|
||||||
|
|
||||||
public logout(): void {
|
|
||||||
this.keycloak.logout();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isLoggedIn(): Promise<boolean> {
|
|
||||||
return this.keycloak.isLoggedIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRoles(): string[] {
|
|
||||||
return this.keycloak.getUserRoles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Crear un guardia para rutas protegidas
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ng generate guard auth
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Edita `src/app/auth.guard.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';
|
|
||||||
import { KeycloakAuthGuard, KeycloakService } from 'keycloak-angular';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root'
|
|
||||||
})
|
|
||||||
export class AuthGuard extends KeycloakAuthGuard {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected override readonly router: Router,
|
|
||||||
protected readonly keycloak: KeycloakService
|
|
||||||
) {
|
|
||||||
super(router, keycloak);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isAccessAllowed(
|
|
||||||
route: ActivatedRouteSnapshot,
|
|
||||||
state: RouterStateSnapshot
|
|
||||||
): Promise<boolean | UrlTree> {
|
|
||||||
|
|
||||||
// Verifica si el usuario está autenticado
|
|
||||||
if (!this.authenticated) {
|
|
||||||
await this.keycloak.login({
|
|
||||||
redirectUri: window.location.origin + state.url,
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Obtiene los roles requeridos desde la ruta
|
|
||||||
const requiredRoles = route.data['roles'];
|
|
||||||
|
|
||||||
// Permite el acceso si no hay roles requeridos
|
|
||||||
if (!requiredRoles || requiredRoles.length === 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifica si el usuario tiene al menos uno de los roles requeridos
|
|
||||||
return requiredRoles.some((role: string) => this.roles.includes(role));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configurar las rutas con protección
|
|
||||||
|
|
||||||
Modifica `src/app/app-routing.module.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
|
||||||
import { AuthGuard } from './auth.guard';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'admin',
|
|
||||||
component: AppComponent,
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
data: { roles: ['administradores'] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'developer',
|
|
||||||
component: AppComponent,
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
data: { roles: ['desarrolladores'] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'user',
|
|
||||||
component: AppComponent,
|
|
||||||
canActivate: [AuthGuard],
|
|
||||||
data: { roles: ['usuarios'] }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: AppComponent
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [RouterModule.forRoot(routes)],
|
|
||||||
exports: [RouterModule]
|
|
||||||
})
|
|
||||||
export class AppRoutingModule { }
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modificar el componente principal
|
|
||||||
|
|
||||||
Edita `src/app/app.component.ts`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Component, OnInit } from '@angular/core';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { KeycloakProfile } from 'keycloak-js';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-root',
|
|
||||||
templateUrl: './app.component.html',
|
|
||||||
styleUrls: ['./app.component.css']
|
|
||||||
})
|
|
||||||
export class AppComponent implements OnInit {
|
|
||||||
title = 'angular-keycloak-app';
|
|
||||||
isLoggedIn = false;
|
|
||||||
userProfile: KeycloakProfile | null = null;
|
|
||||||
userRoles: string[] = [];
|
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
|
||||||
|
|
||||||
async ngOnInit() {
|
|
||||||
this.isLoggedIn = await this.authService.isLoggedIn();
|
|
||||||
|
|
||||||
if (this.isLoggedIn) {
|
|
||||||
this.userProfile = await this.authService.getLoggedUser();
|
|
||||||
this.userRoles = this.authService.getRoles();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
login() {
|
|
||||||
this.authService.login();
|
|
||||||
}
|
|
||||||
|
|
||||||
logout() {
|
|
||||||
this.authService.logout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Edita `src/app/app.component.html`:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h2>Demo Keycloak con LDAP - Correos.com</h2>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div *ngIf="isLoggedIn; else loginButton">
|
|
||||||
<h3>Bienvenido, {{ userProfile?.firstName }} {{ userProfile?.lastName }}</h3>
|
|
||||||
<p>Email: {{ userProfile?.email }}</p>
|
|
||||||
<p>Nombre de Usuario: {{ userProfile?.username }}</p>
|
|
||||||
|
|
||||||
<h4>Tus Roles:</h4>
|
|
||||||
<ul>
|
|
||||||
<li *ngFor="let role of userRoles">{{ role }}</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<button class="btn btn-danger" (click)="logout()">Cerrar Sesión</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #loginButton>
|
|
||||||
<p>Por favor inicia sesión para acceder al sistema</p>
|
|
||||||
<button class="btn btn-primary" (click)="login()">Iniciar Sesión</button>
|
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Agrega Bootstrap a `src/index.html`:
|
|
||||||
|
|
||||||
```html
|
|
||||||
<head>
|
|
||||||
<!-- Otras etiquetas -->
|
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Ejecutar la aplicación
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ng serve
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Abre http://localhost:4200 en tu navegador.
|
|
||||||
|
|
||||||
## 7. Arquitectura del sistema
|
|
||||||
|
|
||||||
La arquitectura del sistema está compuesta por los siguientes componentes:
|
|
||||||
|
|
||||||
- **OpenLDAP**: Directorio de usuarios y grupos (puerto 389)
|
|
||||||
- **phpLDAPadmin**: Interfaz gráfica para administrar LDAP (puerto 80)
|
|
||||||
- **Keycloak**: Servidor de autenticación y autorización (puerto 8080)
|
|
||||||
- **Aplicación Angular**: Cliente que utiliza el SSO de Keycloak (puerto 4200)
|
|
||||||
|
|
||||||
La estructura del directorio LDAP para correos.com es:
|
|
||||||
|
|
||||||
- dc=correos,dc=com (raíz del directorio)
|
|
||||||
- ou=usuarios (unidad organizativa para usuarios)
|
|
||||||
- uid=admin (usuario administrador)
|
|
||||||
- uid=developer (usuario desarrollador)
|
|
||||||
- uid=user (usuario normal)
|
|
||||||
- ou=grupos (unidad organizativa para grupos)
|
|
||||||
- cn=administradores (grupo de administradores)
|
|
||||||
- cn=desarrolladores (grupo de desarrolladores)
|
|
||||||
- cn=usuarios (grupo de usuarios)
|
|
||||||
|
|
||||||
## 8. Resolución de problemas comunes
|
|
||||||
|
|
||||||
### Problema: "ldap_bind: Invalid credentials (49)"
|
|
||||||
|
|
||||||
Este error ocurre cuando intentas conectarte a LDAP con credenciales incorrectas. Para resolverlo:
|
|
||||||
|
|
||||||
1. Verifica que estás usando el dominio correcto: `dc=correos,dc=com`
|
|
||||||
2. Asegúrate de usar el DN correcto: `cn=admin,dc=correos,dc=com`
|
|
||||||
3. Comprueba que la contraseña de administrador es correcta
|
|
||||||
|
|
||||||
Si olvidaste la contraseña, puedes restablecerla:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo dpkg-reconfigure slapd
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
O también:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo slappasswd
|
|
||||||
# Copia el hash generado
|
|
||||||
sudo nano /etc/ldap/slapd.d/cn=config/olcDatabase={1}mdb.ldif
|
|
||||||
# Busca olcRootPW y reemplaza el valor con el nuevo hash
|
|
||||||
sudo systemctl restart slapd
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: No puedes conectarte a LDAP en absoluto
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verifica que el servicio esté ejecutándose
|
|
||||||
sudo systemctl status slapd
|
|
||||||
|
|
||||||
# Reinicia el servicio si es necesario
|
|
||||||
sudo systemctl restart slapd
|
|
||||||
|
|
||||||
# Verifica que el puerto esté abierto
|
|
||||||
sudo netstat -tulpn | grep 389
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: Keycloak no se inicia
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verifica los logs
|
|
||||||
sudo journalctl -u keycloak
|
|
||||||
|
|
||||||
# Asegúrate de que Java esté instalado correctamente
|
|
||||||
java -version
|
|
||||||
|
|
||||||
# Verifica los permisos
|
|
||||||
sudo ls -la /opt/keycloak
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problema: La aplicación Angular no puede conectarse a Keycloak
|
|
||||||
|
|
||||||
1. Verifica que Keycloak esté en ejecución
|
|
||||||
2. Comprueba la URL del servidor Keycloak en la configuración de Angular
|
|
||||||
3. Asegúrate de que el cliente esté correctamente configurado en Keycloak
|
|
||||||
4. Verifica el Client Secret
|
|
||||||
5. Comprueba las URLs de redirección
|
|
||||||
|
|
||||||
## 9. Verificación y prueba del sistema
|
|
||||||
|
|
||||||
### Verificar que Keycloak esté sincronizando correctamente con LDAP
|
|
||||||
|
|
||||||
1. Inicia sesión en la consola de administración de Keycloak (http://localhost:8080/admin/)
|
|
||||||
2. Navega a "Users" en el reino "angular-app"
|
|
||||||
3. Deberías ver los usuarios de LDAP: admin, developer y user
|
|
||||||
4. Navega a "Groups" y verifica que los grupos de LDAP estén presentes
|
|
||||||
|
|
||||||
### Probar la aplicación Angular
|
|
||||||
|
|
||||||
1. Asegúrate de que Keycloak esté ejecutándose
|
|
||||||
2. Inicia la aplicación Angular con `ng serve`
|
|
||||||
3. Navega a http://localhost:4200
|
|
||||||
4. Haz clic en "Iniciar Sesión"
|
|
||||||
5. Deberías ser redirigido a la pantalla de inicio de sesión de Keycloak
|
|
||||||
6. Inicia sesión con uno de los usuarios LDAP:
|
|
||||||
- Usuario: admin, Contraseña: password123
|
|
||||||
- Usuario: developer, Contraseña: password123
|
|
||||||
- Usuario: user, Contraseña: password123
|
|
||||||
7. Después de iniciar sesión, serás redirigido de vuelta a la aplicación
|
|
||||||
8. La aplicación mostrará tu perfil y roles
|
|
||||||
|
|
||||||
## 10. Resumen
|
|
||||||
|
|
||||||
Esta configuración proporciona un sistema completo de gestión de identidades y acceso con:
|
|
||||||
|
|
||||||
- **Autenticación centralizada**: Los usuarios solo necesitan recordar un conjunto de credenciales
|
|
||||||
- **Gestión de usuarios unificada**: Todos los usuarios se administran en LDAP
|
|
||||||
- **Control de acceso basado en roles**: Las rutas y funcionalidades pueden ser protegidas según los roles del usuario
|
|
||||||
- **Experiencia de inicio de sesión único (SSO)**: Una vez autenticado, el usuario puede acceder a todas las aplicaciones integradas
|
|
||||||
|
|
||||||
Este sistema es adecuado para entornos empresariales donde se requiere un control de acceso detallado y una gestión centralizada de usuarios.
|
|
||||||
|
|
||||||
La implementación es nativa en un sistema Ubuntu, lo que la hace ideal para despliegues en servidores VPS o entornos similares sin necesidad de contenedores.
|
|
||||||
Loading…
x
Reference in New Issue
Block a user