Overview
The LoginUserQuery is a CQRS query object that encapsulates the data required to authenticate a user in the system. It follows the Query pattern, separating the request for user authentication from the actual execution logic.
CQRS Pattern
This implementation follows the Command Query Responsibility Segregation (CQRS) pattern:
- Queries (like
LoginUserQuery) represent read operations that retrieve data without changing system state
- Query Handlers (like
LoginUserHandler) contain the business logic to execute queries
- This separation provides better testability, maintainability, and allows independent scaling of read and write operations
Command vs Query
| Aspect | Commands | Queries |
|---|
| Purpose | Modify state | Retrieve data |
| Return Value | Usually void or confirmation | Data objects |
| Side Effects | Yes | No (idempotent) |
| Example | RegisterUserCommand | LoginUserQuery |
LoginUserQuery Class
Source Code
export class LoginUserQuery {
constructor(readonly usuario: string, readonly contrasena: string) {}
}
Location: src/app/usuario/application/queries/login-user.query.ts
Parameters
The username of the user attempting to log in. This is passed as a readonly property to ensure immutability.
The password provided for authentication. This is passed as a readonly property to ensure immutability.
Properties
The query has two readonly properties that are set via the constructor:
usuario: The username string
contrasena: The password string
Both properties are readonly to ensure query immutability, which is a key principle in CQRS.
LoginUserHandler
Source Code
import { Observable } from "rxjs";
import { UsuarioRepository } from "../../../domain/repositories/usuario.repository";
import { LoginUserQuery } from "../../queries/login-user.query";
import { Usuario } from "../../../domain/models/usuario";
import { Injectable } from "@angular/core";
@Injectable()
export class LoginUserHandler {
constructor(private usuarioRepository: UsuarioRepository){}
handle(query: LoginUserQuery): Observable<Usuario> {
return this.usuarioRepository.loginUsuario(query.usuario, query.contrasena);
}
}
Location: src/app/usuario/application/usecases/queryHandlers/login-user.handler.ts
Handler Method
handle(query: LoginUserQuery): Observable<Usuario>
Executes the user login query to authenticate credentials.
Parameters:
query: The LoginUserQuery instance containing authentication credentials
Returns:
Observable<Usuario>: An RxJS Observable that emits the authenticated Usuario object if credentials are valid
Flow:
- Receives the query with user credentials
- Delegates to the
UsuarioRepository to verify credentials
- Returns an Observable of the authenticated user
- If authentication fails, the Observable will emit an error
Dependencies
usuarioRepository
UsuarioRepository
required
Repository interface that handles user authentication and data retrieval operations. Injected via Angular’s dependency injection.
Usage Example
Creating and Executing the Query
import { LoginUserQuery } from '@app/usuario/application/queries/login-user.query';
import { LoginUserHandler } from '@app/usuario/application/usecases/queryHandlers/login-user.handler';
// In your authentication service
export class AuthenticationService {
constructor(private loginUserHandler: LoginUserHandler) {}
login(username: string, password: string): Observable<Usuario> {
// Create the query
const query = new LoginUserQuery(username, password);
// Execute via the handler
return this.loginUserHandler.handle(query);
}
}
In an Angular Component
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LoginUserHandler } from '@app/usuario/application/usecases/queryHandlers/login-user.handler';
import { LoginUserQuery } from '@app/usuario/application/queries/login-user.query';
@Component({
selector: 'app-login',
template: `
<form (ngSubmit)="onLogin()">
<input [(ngModel)]="username" placeholder="Username" />
<input [(ngModel)]="password" type="password" placeholder="Password" />
<button type="submit">Login</button>
<div *ngIf="error" class="error">{{ error }}</div>
</form>
`
})
export class LoginComponent {
username = '';
password = '';
error = '';
constructor(
private loginUserHandler: LoginUserHandler,
private router: Router
) {}
onLogin(): void {
const query = new LoginUserQuery(this.username, this.password);
this.loginUserHandler.handle(query).subscribe({
next: (user) => {
console.log('Login successful:', user);
// Store authentication token/session
localStorage.setItem('currentUser', JSON.stringify(user));
// Navigate to dashboard
this.router.navigate(['/dashboard']);
},
error: (err) => {
console.error('Login failed:', err);
this.error = 'Invalid username or password';
}
});
}
}
With RxJS Operators
import { map, catchError } from 'rxjs/operators';
import { of } from 'rxjs';
export class LoginService {
constructor(private loginUserHandler: LoginUserHandler) {}
authenticateUser(username: string, password: string): Observable<AuthResult> {
const query = new LoginUserQuery(username, password);
return this.loginUserHandler.handle(query).pipe(
map(user => ({
success: true,
user: user,
token: this.generateToken(user)
})),
catchError(error => of({
success: false,
error: error.message
}))
);
}
private generateToken(user: Usuario): string {
// Token generation logic
return btoa(JSON.stringify({ id: user.id, timestamp: Date.now() }));
}
}
Architecture Flow
Usuario Interface
export interface Usuario {
id: number,
usuario: string,
contrasena: string
}
Note: In a production system, the password should not be returned in the Usuario object after authentication. Consider creating a separate AuthenticatedUser type that excludes sensitive data.
UsuarioRepository Interface
export abstract class UsuarioRepository {
abstract loginUsuario(usuario: string, contrasena: string): Observable<Usuario>;
abstract registrarUsuario(usuario: string, contrasena: string): Observable<Usuario>;
}
Error Handling
The handler returns an Observable, which allows for sophisticated error handling:
this.loginUserHandler.handle(query).subscribe({
next: (user) => {
// Handle successful authentication
},
error: (error) => {
// Handle different error types
if (error.status === 401) {
console.error('Invalid credentials');
} else if (error.status === 404) {
console.error('User not found');
} else {
console.error('Login error:', error);
}
}
});
Security Considerations
- Password Handling: Passwords should be hashed before storage and comparison
- Rate Limiting: Implement rate limiting to prevent brute force attacks
- Session Management: Use secure tokens (JWT) instead of storing sensitive user data
- HTTPS Only: Always transmit credentials over secure connections
- Password Validation: Enforce strong password requirements
Best Practices
- Immutability: Query properties are readonly to prevent modification after creation
- Single Responsibility: The query only carries data; the handler contains logic
- Dependency Injection: Handler receives repository via Angular DI
- Observable Pattern: Returns Observable for async operations and reactive programming
- Separation of Concerns: Clear separation between query creation and execution
- Idempotent Operations: Queries should not modify state (read-only operations)
Benefits of This Pattern
- Testability: Easy to unit test handlers independently from infrastructure
- Maintainability: Clear structure and separation of concerns
- Scalability: Read operations can be scaled independently from write operations
- Reusability: Queries can be reused across different parts of the application
- Type Safety: Strong typing ensures compile-time checks
- Observable Pattern: Enables reactive programming and easy composition
Testing Example
import { TestBed } from '@angular/core/testing';
import { LoginUserHandler } from './login-user.handler';
import { LoginUserQuery } from '../../queries/login-user.query';
import { UsuarioRepository } from '../../../domain/repositories/usuario.repository';
import { of } from 'rxjs';
describe('LoginUserHandler', () => {
let handler: LoginUserHandler;
let mockRepository: jasmine.SpyObj<UsuarioRepository>;
beforeEach(() => {
mockRepository = jasmine.createSpyObj('UsuarioRepository', ['loginUsuario']);
TestBed.configureTestingModule({
providers: [
LoginUserHandler,
{ provide: UsuarioRepository, useValue: mockRepository }
]
});
handler = TestBed.inject(LoginUserHandler);
});
it('should authenticate valid user', (done) => {
const mockUser = { id: 1, usuario: 'testuser', contrasena: 'hashed' };
mockRepository.loginUsuario.and.returnValue(of(mockUser));
const query = new LoginUserQuery('testuser', 'password123');
handler.handle(query).subscribe(user => {
expect(user).toEqual(mockUser);
expect(mockRepository.loginUsuario).toHaveBeenCalledWith('testuser', 'password123');
done();
});
});
});
See Also