Skip to main content

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

AspectCommandsQueries
PurposeModify stateRetrieve data
Return ValueUsually void or confirmationData objects
Side EffectsYesNo (idempotent)
ExampleRegisterUserCommandLoginUserQuery

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

usuario
string
required
The username of the user attempting to log in. This is passed as a readonly property to ensure immutability.
contrasena
string
required
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:
  1. Receives the query with user credentials
  2. Delegates to the UsuarioRepository to verify credentials
  3. Returns an Observable of the authenticated user
  4. 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

  1. Password Handling: Passwords should be hashed before storage and comparison
  2. Rate Limiting: Implement rate limiting to prevent brute force attacks
  3. Session Management: Use secure tokens (JWT) instead of storing sensitive user data
  4. HTTPS Only: Always transmit credentials over secure connections
  5. Password Validation: Enforce strong password requirements

Best Practices

  1. Immutability: Query properties are readonly to prevent modification after creation
  2. Single Responsibility: The query only carries data; the handler contains logic
  3. Dependency Injection: Handler receives repository via Angular DI
  4. Observable Pattern: Returns Observable for async operations and reactive programming
  5. Separation of Concerns: Clear separation between query creation and execution
  6. 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

Build docs developers (and LLMs) love