Componentes, servicios, constantes y configuración de rutas para el flujo de inicio de sesión. Incluye manejo de errores, validaciones, diseño responsive y comunicación con el backend OAuth2.
🔑 LoginService + OAuth2
📝 Formulario reactivo
📱 Responsive (mobile/tablet)
⚠️ Manejo de errores
🔄 Validación de token
login.constant.ts
export const ERROR\_LOGIN\_MESSAGES = {
VALIDATION: {
USER\_REQUIRED: 'El Usuario es requerido',
USER\_INVALID: 'Ingresa un Usuario válido',
PASSWORD\_REQUIRED: 'La contraseña es requerida',
PASSWORD\_MIN\_LENGTH: (length: number) =>
\`La contraseña debe tener al menos ${length} caracteres\`,
REQUIRED: 'Este campo es obligatorio',
},
ERROR: {
AUTHENTICATION\_MESSAGE: 'Error en la autenticación',
UNAUTHORIZED: {
TITLE: 'Credenciales incorrectas',
SUBTITLE: 'Verifica usuario y contraseña.',
},
GENERIC: {
TITLE: 'Error de conexión con el servidor',
SUBTITLE: 'Intenta nuevamente',
},
},
} as const;
Centralización de mensajes de error y validación
data-access/login.service.ts
@Injectable({ providedIn: 'root' })
export class LoginService {
private readonly http = inject(HttpService);
private readonly authService = inject(AuthService);
public login(payload: LoginPayload): Observable<AuthContent> {
const loginPayload = {
username: payload.user.trim().toLowerCase(),
password: payload.password.trim(),
};
return this.http.post<AuthContent>('/simappe-oauth2-server/api/v1/login', loginPayload).pipe(
map((response: ApiResponse<AuthContent>) => response.response.content),
tap((content) => this.authService.handleSuccessfulAuth(content)),
catchError((error: HttpErrorResponse) => {
let errorMessage = 'Error en la autenticación';
if (error.status === 401) errorMessage = ERROR\_LOGIN\_MESSAGES.ERROR.UNAUTHORIZED.TITLE;
else if (error.status === 0) errorMessage = ERROR\_LOGIN\_MESSAGES.ERROR.GENERIC.TITLE;
return throwError(() => errorMessage);
}),
);
}
}
** Servicio de autenticación con OAuth2**
✔️ Endpoint: /simappe-oauth2-server/api/v1/login
✔️ Normalización: usuario a minúsculas y trim, contraseña solo trim.
✔️ Manejo de respuesta: extrae response.response.content (estructura ApiResponse).
✔️ tap handleSuccessfulAuth: guarda token y datos de sesión en localStorage/ servicios.
✔️ catchError: mapea errores HTTP a mensajes amigables usando ERROR_LOGIN_MESSAGES.
✔️ HTTP 401: credenciales incorrectas → mensaje específico.
✔️ HTTP 0: error de red / servidor caído.
domain/login.model.ts
export interface LoginPayload {
user: string;
password: string;
}
export interface ErrorLogin {
TITLE: string;
SUBTITLE: string;
}
** Tipado fuerte para el flujo de login**
✔️ LoginPayload: define la estructura que espera el LoginService.
✔️ ErrorLogin: interfaz para errores mostrados al usuario (título + subtítulo).
✔️ Utilizado por LoginComponent para el signal de error.
feature-login/login.component.ts
@Component({
selector: 'nebula-login',
standalone: true,
imports: \[ReactiveFormsModule, InputComponent, ButtonComponent, AlertComponent, ...\],
templateUrl: './login.component.html',
styleUrls: \['./login.component.scss'\],
})
export class LoginComponent implements OnInit, OnDestroy {
form!: FormGroup;
loading = signal(false);
error = signal<{ TITLE: string; SUBTITLE: string } | null>(null);
isMobile = false;
isTablet = false;
onSubmit(): void {
if (this.form.valid) {
this.loading.set(true);
this.loginService.login(payload).subscribe({
next: () => {
this.auth.validateTokenAndGetSession().subscribe({
next: () => this.router.navigate(\['/layout/company'\]),
error: (e) => this.showError(e)
});
},
error: (err) => this.showError(err)
});
}
}
}
Componente standalone con Angular Material + nebula-ui-kit
login.component.html
<div class="login-container">
<div class="background-overlay"></div>
<div class="content-wrapper" \[class.mobile-view\]="isMobile">
@if (!isMobile) {
<form \[formGroup\]="form" class="login-form">
<div class="brand-section">
<img src="assets/shell/logos/logo-nebula.svg" alt="Nebula ERP" />
</div>
<lib-input formControlName="user" label="Usuario"></lib-input>
<lib-input formControlName="password" type="password" label="Clave"></lib-input>
<lib-button \[loading\]="loading()" (clicked)="onSubmit()">
{{ loading() ? 'Entrando...' : 'Ingresar' }}
</lib-button>
</form>
}
@if (isMobile) {
<div class="mobile-message">
<p>Accede desde computadora o tablet</p>
</div>
}
</div>
</div>
📄 UI responsive con degradado y bloqueo en móvil
✔️ Fondo con overlay: imagen de red/patrón de fondo (network-background).
✔️ Logo Nebula: desde assets/shell/logos/logo-nebula.svg.
✔️ Componentes reutilizables: lib-input y lib-button de nebula-ui-kit.
✔️ Bloqueo en móvil: si isMobile = true, muestra mensaje informativo en lugar del formulario.
✔️ Alertas de error: se muestran debajo del formulario con lib-alert (tipo error).
✔️ Enlace "olvidé mi clave": placeholder (href="#") para futura funcionalidad.
login-routes.ts
import { Routes } from '@angular/router';
import { LoginComponent } from './feature-login/src/lib/login/login.component';
export const LOGIN\_ROUTES: Routes = \[
{
path: '',
component: LoginComponent,
},
{
path: '\*\*',
redirectTo: '',
},
\];
🗺️ Rutas standalone del módulo de login
✔️ Ruta raíz ('') → LoginComponent.
✔️ Wildcard ('**') redirige a raíz (evita 404 en rutas desconocidas dentro del módulo).
✔️ Diseñado para lazy loading desde la aplicación principal (loadChildren: () => import('@nebula/login').then(m => m.LOGIN_ROUTES)).
index.ts
export { LoginComponent } from './lib/feature-login/src/lib/login/login.component';
export { LOGIN\_ROUTES } from './lib/login-routes';
export { LoginService } from './lib/data-access/src/lib/login.service';
export { ERROR\_LOGIN\_MESSAGES } from './lib/const/src/lib/login.constant';
🚪 Exportaciones para consumo externo
✔️ LoginComponent: componente standalone para mostrar en ruta.
✔️ LOGIN_ROUTES: configuración de rutas para lazy loading.
✔️ LoginService: servicio de autenticación (puede ser usado por otros módulos, ej. para refrescar token).
✔️ ERROR_LOGIN_MESSAGES: constantes reutilizables en toda la aplicación.
Secuencia completa
📋 Paso a paso:
⚠️ Manejo de errores:
- HTTP 401 → "Credenciales incorrectas"
- HTTP 0 → "Error de conexión con el servidor"
- Otros errores → mensaje genérico
🔐 Seguridad adicional: El login normaliza usuario a minúsculas, evita inyección de espacios. La contraseña se envía en texto plano sobre HTTPS. El token JWT retornado se almacena en localStorage y se incluye en headers de todas las peticiones posteriores.
Adaptativo mobile/tablet
📐 Breakpoints implementados:
✔️ checkScreenSize() en ngOnInit + event listener de resize.
✔️ Signals isMobile / isTablet para renderizado condicional en template.
📁 const/
login.constant.ts → mensajes de error y validación.
📁 domain/
login.model.ts → LoginPayload, ErrorLogin interfaces.
📁 data-access/
LoginService → comunicación con API OAuth2.
🎨 feature-login/
LoginComponent standalone, template y estilos.
✨ Mejores prácticas implementadas: Signals para estado reactivo, formularios reactivos, separación de responsabilidades (servicio de API vs componente UI), constantes centralizadas, manejo de errores HTTP amigable, lazy loading ready, diseño responsive y accesible.
🔐 @nebula/login · Módulo de autenticación completo con OAuth2, manejo de errores, responsive y fácil integración con el resto de la aplicación Nebula ERP.