Skip to main content

Services Overview

The Trippins frontend uses 6 core services to handle business logic, HTTP communication, authentication, and UI interactions. All services use Angular’s @Injectable decorator with providedIn: 'root' for singleton instances.

Authentication

AuthService

Housing Management

HousingServiceService

Reservations

ReservationServiceService

Reviews

ReviewServiceService

User Management

UserServiceService

Layout/UI

LayoutService

Authentication Service

Handles user authentication, JWT token management, and role-based authorization.

Service Implementation

src/app/services/auth.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, catchError, map, of, tap } from 'rxjs';
import { AuthenticationRequest, AuthenticationResponse, UserDTO } from '../models/DTOS/user-dto';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  private readonly JWT_KEY = 'auth_jwt';
  private readonly ROLES_KEY = 'user_roles';
  private username: string | null = null;

  constructor(private http: HttpClient) {}

  login(credentials: { email: string; password: string }): Observable<AuthenticationResponse> {
    return this.http.post<AuthenticationResponse>(
      `${environment.baseUrlApi}/login`, 
      credentials
    ).pipe(
      tap(response => {
        this.storeJwt(response.jwt);
        this.storeRoles(response.roles);
        this.username = credentials.email;
      })
    );
  }

  private storeJwt(jwt: string): void {
    localStorage.setItem(this.JWT_KEY, jwt);
  }

  private storeRoles(roles: string[]): void {
    localStorage.setItem(this.ROLES_KEY, JSON.stringify(roles));
  }

  getJwt(): string | null {
    return localStorage.getItem(this.JWT_KEY);
  }

  getRoles(): string[] {
    const rolesJson = localStorage.getItem(this.ROLES_KEY);
    return rolesJson ? JSON.parse(rolesJson) : [];
  }

  hasRole(role: string): boolean {
    return this.getRoles().includes(role);
  }

  hasAnyRole(roles: string[]): boolean {
    const currentRoles: string[] = this.getRoles();
    for (let index = 0; index < roles.length; index++) {
      if (currentRoles.includes(roles[index])) {
        return true;
      }
    }
    return false;
  }

  isLoggedIn(): boolean {
    return !!this.getJwt();
  }

  logout(): void {
    localStorage.removeItem(this.JWT_KEY);
    localStorage.removeItem(this.ROLES_KEY);
  }

  getUsername(): string | null {
    if (this.username) return this.username;
    
    // Fallback to JWT parsing if username not set
    const token = this.getJwt();
    if (!token) return null;
    
    try {
      const payload = JSON.parse(atob(token.split('.')[1]));
      return payload.sub || payload.username || null;
    } catch (e) {
      return null;
    }
  }
  
  updateUsername(newUsername: string): void {
    this.username = newUsername;
  }

  getUserDni(): Observable<string | null> {
    const usernameemail = this.getUsername();
    return this.http.get<UserDTO[]>(`${environment.baseUrlApi}/users`).pipe(
      map(users => {
        const user = users.find(u => u.email === usernameemail);
        return user ? user.dni : null;
      }),
      catchError(err => {
        console.error('Error loading user data:', err);
        return of(null);
      })
    );
  }

  getUserName(): Observable<string | null> {
    const usernameemail = this.getUsername();
    return this.http.get<UserDTO[]>(`${environment.baseUrlApi}/users`).pipe(
      map(users => {
        const user = users.find(u => u.email === usernameemail);
        return user ? user.name : null;
      }),
      catchError(err => {
        console.error('Error loading user data:', err);
        return of(null);
      })
    );
  }
}

AuthService Methods

Purpose: Authenticate user and store JWT tokenParameters:
  • credentials: Object with email and password
Returns: Observable<AuthenticationResponse>Side Effects:
  • Stores JWT token in localStorage
  • Stores user roles in localStorage
  • Sets username in memory
Purpose: Retrieve stored JWT tokenReturns: string | nullUsage: Used by AuthInterceptor to attach token to requests
Purpose: Get user’s roles from localStorageReturns: string[] (e.g., ["ROLE_USER", "ROLE_ADMIN"])Usage: Role-based UI rendering and authorization
Purpose: Check if user has specific role(s)Parameters:
  • role: Single role string
  • roles: Array of role strings
Returns: booleanUsage: Guards, conditional rendering, authorization logic
Purpose: Check if user is authenticatedReturns: booleanUsage: Route guards, header navigation display
Purpose: Clear authentication dataSide Effects:
  • Removes JWT from localStorage
  • Removes roles from localStorage
Purpose: Fetch additional user details from APIReturns: Observable<string | null>Usage: Profile component, reservation forms

Usage Example

import { AuthService } from '../../services/auth.service';

export class LoginComponent {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  onSubmit(): void {
    const { email, password } = this.loginForm.value;
    
    this.authService.login({ email, password }).subscribe({
      next: () => {
        this.router.navigate(['/']);
      },
      error: (err) => {
        this.errorMessage = 'Invalid credentials';
      }
    });
  }
}

Housing Service

Manages hotel/housing listings, search, and image uploads.
src/app/services/housing-service.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { HousingDTO, PagedResponse } from '../models/DTOS/housing-dto';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class HousingServiceService {
  constructor(private http: HttpClient) {}

  getRooms(page: number, size: number): Observable<PagedResponse<HousingDTO>> {
    return this.http.get<PagedResponse<HousingDTO>>(
      `${environment.baseUrlApi}/rooms/extra?page=${page}&size=6`
    );
  }

  searchHouses(tags: string, stars: number): Observable<any[]> {
    return this.http.get<any[]>(
      `${environment.baseUrlApi}/query?tags=${tags}&stars=${stars}`
    );
  }

  getSpecificRoom(code: number): Observable<HousingDTO> {
    return this.http.get<HousingDTO>(
      `${environment.baseUrlApi}/houses/${code}`
    );
  }

  createRoom(data: any): Observable<HousingDTO> {
    return this.http.post<HousingDTO>(
      `${environment.baseUrlApi}/houses`, 
      data
    );
  }

  uploadHousingImage(code: number, image: File): Observable<HousingDTO> {
    const formData = new FormData();
    formData.append('file', image);
    return this.http.put<HousingDTO>(
      `${environment.baseUrlApi}/houses/${code}/image`, 
      formData
    );
  }
}

HousingService Methods

MethodEndpointPurposeReturn Type
getRooms(page, size)GET /rooms/extraPaginated hotel listingsObservable<PagedResponse<HousingDTO>>
searchHouses(tags, stars)GET /querySearch by tags and ratingObservable<HousingDTO[]>
getSpecificRoom(code)GET /houses/{code}Single hotel detailsObservable<HousingDTO>
createRoom(data)POST /housesCreate new hotelObservable<HousingDTO>
uploadHousingImage(code, image)PUT /houses/{code}/imageUpload hotel imageObservable<HousingDTO>

Usage Example

Room Component
export class RoomComponent implements OnInit {
  houses: any[] = [];
  currentPage = -1;
  pageSize = 6;
  hasMore = true;

  constructor(private houseService: HousingServiceService) {}

  ngOnInit(): void {
    this.loadHouses();
  }

  loadHouses(): void {
    if (!this.hasMore) return;
    this.currentPage++;

    this.houseService.getRooms(this.currentPage, this.pageSize).subscribe({
      next: (response) => {
        this.houses = [...this.houses, ...response.content];
        this.hasMore = !response.last;
      },
      error: (err) => {
        console.error('Error loading houses:', err);
      }
    });
  }

  onSearchSubmit(): void {
    this.houseService.searchHouses(this.tags, this.stars).subscribe({
      next: (houses) => {
        this.houses = houses;
      }
    });
  }
}
The PagedResponse interface matches Spring Boot’s pagination structure with content, last, totalPages, etc.

Reservation Service

Handles booking creation and retrieval.
src/app/services/reservation-service.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment.development';
import { Observable } from 'rxjs';
import { ReservationDTO } from '../models/DTOS/reservation-dto';

@Injectable({
  providedIn: 'root'
})
export class ReservationServiceService {
  constructor(private http: HttpClient) {}

  createReservation(data: any): Observable<ReservationDTO> {
    return this.http.post<ReservationDTO>(
      `${environment.baseUrlApi}/reservations`, 
      data
    );
  }

  getAllReservations(): Observable<ReservationDTO[]> {
    return this.http.get<ReservationDTO[]>(
      `${environment.baseUrlApi}/reservations`
    );
  }
}

Usage Example

RoomDetails Component
export class RoomDetailsComponent {
  reservationForm: FormGroup;

  constructor(
    private fb: FormBuilder,
    private reservationService: ReservationServiceService,
    private authService: AuthService
  ) {
    this.reservationForm = this.fb.group({
      checkIn: ['', Validators.required],
      checkOut: ['', Validators.required]
    });
  }

  onSubmitReservation(): void {
    if (this.reservationForm.invalid || !this.house) return;

    const reservationData = {
      ...this.reservationForm.value,
      housingCode: this.house.code,
      housingName: this.house.name,
      clientDni: this.clientDni,
      valorated: false
    };

    this.reservationService.createReservation(reservationData).subscribe({
      next: () => {
        Swal.fire({
          title: '¡Reserva exitosa! 🎉',
          icon: 'success'
        });
        this.reservationForm.reset();
      },
      error: (err) => {
        Swal.fire({
          title: 'Error creating reservation',
          icon: 'error'
        });
      }
    });
  }
}

Review Service

Manages hotel reviews and ratings.
src/app/services/review-service.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { PagedResponse } from '../models/DTOS/housing-dto';
import { ReviewDTO } from '../models/DTOS/review-dto';
import { environment } from '../../environments/environment.development';

@Injectable({
  providedIn: 'root'
})
export class ReviewServiceService {
  constructor(private http: HttpClient) {}

  getPaginatedComments(code: number, page: number): Observable<PagedResponse<ReviewDTO>> {
    return this.http.get<PagedResponse<ReviewDTO>>(
      `${environment.baseUrlApi}/rooms/${code}/comments/extra?page=${page}&size=6`
    );
  }
  
  createReview(commentData: any): Observable<ReviewDTO> {
    return this.http.post<ReviewDTO>(
      `${environment.baseUrlApi}/reviews`, 
      commentData
    );
  }
}

Usage Example

RoomDetails Component
export class RoomDetailsComponent {
  comments: ReviewDTO[] = [];
  commentForm: FormGroup;

  constructor(private reviewService: ReviewServiceService) {
    this.commentForm = this.fb.group({
      comment: ['', [Validators.required, Validators.minLength(10)]],
      rating: [50, [Validators.required, Validators.min(0), Validators.max(100)]]
    });
  }

  loadComments(code: number, page: number): void {
    this.reviewService.getPaginatedComments(code, page).subscribe({
      next: (response) => {
        this.comments = [...this.comments, ...response.content];
        this.currentPage = page;
      }
    });
  }

  onSubmitComment(): void {
    const commentData = {
      ...this.commentForm.value,
      hotelCode: this.house.code,
      userDni: this.clientDni,
      userName: this.clientName
    };

    this.reviewService.createReview(commentData).subscribe({
      next: () => {
        Swal.fire({ title: 'Review submitted!', icon: 'success' });
        this.loadComments(this.house!.code, 1);
      }
    });
  }
}

User Service

Handles user account operations.
src/app/services/user-service.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { UserDTO } from '../models/DTOS/user-dto';
import { environment } from '../../environments/environment.development';

@Injectable({
  providedIn: 'root'
})
export class UserServiceService {
  constructor(private http: HttpClient) {}

  createAccount(data: any): Observable<UserDTO> {
    return this.http.post<UserDTO>(
      `${environment.baseUrlApi}/users`,
      data
    );
  }

  updateAccount(data: any, dni: string): Observable<UserDTO> {
    return this.http.put<UserDTO>(
      `${environment.baseUrlApi}/users/${dni}`, 
      data
    );
  }
}

Usage Example

Profile Component
export class ProfileComponent {
  constructor(private userService: UserServiceService) {}

  onSubmit(): void {
    if (this.profileForm.invalid || !this.user) return;

    const updatedUser = {
      ...this.profileForm.value,
      dni: this.user.dni,
      roles: this.user.roles
    };

    this.userService.updateAccount(updatedUser, this.user.dni).subscribe({
      next: () => {
        this.isEditing = false;
        this.user = { ...this.user!, ...updatedUser };
      },
      error: (err) => {
        console.error('Error updating user:', err);
      }
    });
  }
}

Layout Service

Handles UI interactions and jQuery-based animations.
src/app/services/layout.service.ts
import { Injectable } from '@angular/core';
import $ from 'jquery';

@Injectable({
  providedIn: 'root'
})
export class LayoutService {
  initializeDropdownHover() {
    const $dropdown = $(".dropdown");
    const $dropdownToggle = $(".dropdown-toggle");
    const $dropdownMenu = $(".dropdown-menu");
    const showClass = "show";

    $(window).on("load resize", () => {
      if (window.matchMedia("(min-width: 992px)").matches) {
        $dropdown.hover(
          function () {
            const $this = $(this);
            $this.addClass(showClass);
            $this.find($dropdownToggle).attr("aria-expanded", "true");
            $this.find($dropdownMenu).addClass(showClass);
          },
          function () {
            const $this = $(this);
            $this.removeClass(showClass);
            $this.find($dropdownToggle).attr("aria-expanded", "false");
            $this.find($dropdownMenu).removeClass(showClass);
          }
        );
      } else {
        $dropdown.off("mouseenter mouseleave");
      }
    });
  }

  initializeVideoModal() {
    $(document).ready(() => {
      let $videoSrc: string;  
      $('.btn-play').on('click', function () {
        $videoSrc = $(this).data("src");
      });

      $('#videoModal').on('shown.bs.modal', function () {
        $("#video").attr('src', $videoSrc + "?autoplay=1&amp;modestbranding=1&amp;showinfo=0");
      });

      $('#videoModal').on('hide.bs.modal', function () {
        $("#video").attr('src', $videoSrc);
      });
    });
  }

  checkLoggedInCookie() {
    if (document.cookie.includes('justLoggedIn=true')) {
      document.cookie = 'justLoggedIn=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
      setTimeout(() => {
        window.location.reload();
      }, 50);
    }
  }
}
LayoutService bridges Angular with legacy jQuery components like Bootstrap dropdowns and video modals.

HTTP Interceptor

Automatically attaches JWT tokens to all HTTP requests.
src/app/interceptors/auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { AuthService } from '../services/auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getJwt();
    if (token) {
      request = request.clone({
        setHeaders: {
          Authorization: `Bearer ${token}`
        }
      });
    }
    return next.handle(request);
  }
}
  1. Intercepts All Requests: Catches every HTTP request made by the app
  2. Checks for Token: Gets JWT from AuthService
  3. Clones Request: Creates new request with Authorization header
  4. Adds Bearer Token: Formats as Authorization: Bearer <token>
  5. Passes to Next Handler: Continues request pipeline

Interceptor Registration

The interceptor is registered in the app module:
src/app/app.module.ts
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { AuthInterceptor } from './interceptors/auth.interceptor';

@NgModule({
  imports: [HttpClientModule],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ]
})
export class AppModule { }
The multi: true flag allows multiple interceptors to be registered. They execute in the order they’re provided.

Service Communication Patterns

Component → Service → API

1

Component Calls Service

this.housingService.getRooms(0, 6)
2

Service Makes HTTP Request

this.http.get(`${environment.baseUrlApi}/rooms/extra`)
3

Interceptor Adds JWT

request.clone({ setHeaders: { Authorization: `Bearer ${token}` }})
4

Backend Validates Token

Spring Security verifies JWT signature and permissions
5

Response Returns to Component

Component receives Observable data and updates view

Error Handling Pattern

this.housingService.getRooms(page, size).subscribe({
  next: (response) => {
    // Success handling
    this.houses = response.content;
  },
  error: (err) => {
    // Error handling
    console.error('Error loading houses:', err);
    
    if (err.status === 401) {
      // Unauthorized - redirect to login
      this.authService.logout();
      this.router.navigate(['new/login']);
    } else if (err.status === 403) {
      // Forbidden - show error
      this.router.navigate(['new/error']);
    } else {
      // Generic error
      Swal.fire({ title: 'Error', text: err.message, icon: 'error' });
    }
  },
  complete: () => {
    // Optional cleanup
    this.isLoading = false;
  }
});

Data Transfer Objects (DTOs)

Services use TypeScript interfaces for type safety:
export interface UserDTO {
  dni: string;
  name: string;
  number: number;
  email: string;
  admin: boolean;
  roles: UserRole[];
}

export interface AuthenticationResponse {
  jwt: string;
  roles: string[];
}

Service Best Practices

  • Use providedIn: 'root' for app-wide services
  • Services are automatically lazy-loaded
  • Single instance shared across all components
  • Use tap() for side effects (logging, storing tokens)
  • Use map() for data transformation
  • Use catchError() for error handling
  • Use switchMap() for dependent requests
  • Always type Observable return values
  • Define DTOs for all API responses
  • Use interfaces over types
  • Avoid any type
  • Handle errors in components, not services
  • Use consistent error response format
  • Log errors for debugging
  • Provide user-friendly error messages
  • Use environment variables for base URLs
  • Implement retry logic for transient failures
  • Cancel in-flight requests on component destroy
  • Use proper HTTP methods (GET, POST, PUT, DELETE)

Testing Services

Unit Testing Example

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AuthService } from './auth.service';
import { environment } from '../../environments/environment';

describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthService]
    });
    service = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should login and store JWT', () => {
    const mockResponse = {
      jwt: 'mock-token',
      roles: ['ROLE_USER']
    };

    service.login({ email: '[email protected]', password: 'password' }).subscribe(response => {
      expect(response.jwt).toBe('mock-token');
      expect(service.getJwt()).toBe('mock-token');
      expect(service.getRoles()).toEqual(['ROLE_USER']);
    });

    const req = httpMock.expectOne(`${environment.baseUrlApi}/login`);
    expect(req.request.method).toBe('POST');
    req.flush(mockResponse);
  });

  it('should check if user has role', () => {
    localStorage.setItem('user_roles', JSON.stringify(['ROLE_ADMIN']));
    expect(service.hasRole('ROLE_ADMIN')).toBe(true);
    expect(service.hasRole('ROLE_USER')).toBe(false);
  });
});

Next Steps

Component Architecture

See how components use these services

Backend API

Explore API endpoints consumed by services

Build docs developers (and LLMs) love