Skip to main content

Overview

The Happy Habitat authentication system provides comprehensive user authentication, token management, role selection, and session persistence. The system uses JWT tokens with automatic refresh capabilities.

Core Services

AuthService

Location: app/services/auth.service.ts:1 The main authentication service that handles login, registration, and session management. Key Features:
  • User login with username/password
  • User registration
  • Multi-role support with role selection
  • Automatic token refresh
  • Session persistence
  • Mock authentication mode for development
  • Password reset functionality

SessionService

Location: app/services/session.service.ts:1 Manages session storage and token lifecycle. Key Features:
  • Token storage in localStorage
  • Token expiration checking
  • Session validation
  • Secure session cleanup

Authentication Flow

Login Flow

Token Refresh Flow

AuthService API

Login

login(credentials: LoginRequest): Observable<AuthResponse>
Parameters:
interface LoginRequest {
  username: string;
  password: string;
}
Response:
interface AuthResponse {
  token: string;
  refreshToken: string;
  user: UserInfo;
  expiresIn: number; // seconds
}
Example:
this.authService.login({ username: 'admin', password: 'password' })
  .subscribe({
    next: (response) => {
      console.log('Login successful', response.user);
      this.router.navigate(['/dashboard']);
    },
    error: (error) => {
      if (error.needsRoleSelection) {
        this.showRoleSelection(error.roles);
      }
    }
  });

Multi-Role Login

When a user has multiple roles, the login returns an error with role information:
// Initial login
this.authService.login(credentials).subscribe({
  error: (error) => {
    if (error.needsRoleSelection) {
      // Show role selection UI
      this.roles = error.roleDetails;
    }
  }
});

// Complete login with selected role
this.authService.completeLoginWithRole(selectedRoleCode).subscribe({
  next: (response) => {
    // Login completed with selected role
    this.router.navigate(['/dashboard']);
  }
});

Register

register(data: RegisterRequest): Observable<AuthResponse>
Parameters:
interface RegisterRequest {
  firstName: string;
  lastName: string;
  username: string;
  email: string;
  password: string;
  roleId: string;
}
The service automatically logs in the user after successful registration.

Logout

logout(): void
Clears the session and redirects to login:
logout(): void {
  this.sessionService.clearSession();
  this.isAuthenticated.set(false);
  this.currentUser.set(null);
  this.usersService.clearCurrentUser();
  this.router.navigate(['/auth/login']);
}

Token Refresh

refreshToken(): Observable<AuthResponse>
Automatically called by the Auth Interceptor when a 401 error occurs:
refreshToken(): Observable<AuthResponse> {
  const refreshToken = this.sessionService.getRefreshToken();
  
  if (!refreshToken) {
    this.logout();
    return throwError(() => new Error('No refresh token available'));
  }
  
  return this.http.post<AuthResponse>(`${this.API_URL}/refresh`, { refreshToken })
    .pipe(
      tap((response) => this.handleAuthSuccess(response)),
      catchError((error) => {
        this.logout();
        return throwError(() => error);
      })
    );
}

Check Authentication

checkAuth(): boolean
Validates if the user has a valid session:
checkAuth(): boolean {
  const token = this.sessionService.getToken();
  const user = this.sessionService.getUser();
  
  if (token && user && !this.sessionService.isTokenExpired()) {
    this.isAuthenticated.set(true);
    this.currentUser.set(user);
    return true;
  }
  
  this.logout();
  return false;
}

Password Reset

forgotPassword(usernameOrEmail: string): Observable<{ message: string }>
resetPassword(email: string, newPassword: string, token: string): Observable<{ message: string }>
Forgot Password Flow:
this.authService.forgotPassword('[email protected]').subscribe({
  next: (response) => {
    this.showSuccess(response.message);
  }
});
Reset Password:
this.authService.resetPassword(email, newPassword, resetToken).subscribe({
  next: (response) => {
    this.showSuccess('Password updated successfully');
    this.router.navigate(['/auth/login']);
  }
});

Role-Based Access Control

Role Checking

hasRole(role: string): boolean
hasAnyRole(roles: string[]): boolean
Example:
// Check single role
if (this.authService.hasRole(RolesEnum.SYSTEM_ADMIN)) {
  // Admin-only functionality
}

// Check multiple roles
if (this.authService.hasAnyRole([RolesEnum.ADMIN_COMPANY, RolesEnum.SYSTEM_ADMIN])) {
  // Admin functionality
}

Available Roles

export enum RolesEnum {
  SYSTEM_ADMIN = 'SYSTEM_ADMIN',
  ADMIN_COMPANY = 'ADMIN_COMPANY',
  COMITEE_MEMBER = 'COMITEE_MEMBER',
  RESIDENT = 'RESIDENT',
  TENANT = 'RENTER',
  VIGILANCE = 'VIGILANCE'
}

User Information

interface UserInfo {
  id: string;
  fullname: string;
  username: string;
  email: string;
  selectedRole: RolesEnum;      // Current active role
  userRoles: RolesEnum[];        // All available roles
  residentInfo?: ResidentInfo;
}

Session Management

Storage Keys

const TOKEN_KEY = 'hh_token';
const REFRESH_TOKEN_KEY = 'hh_refresh_token';
const USER_KEY = 'hh_user';
const TOKEN_EXPIRY_KEY = 'hh_token_expiry';

Session Storage

saveSession(authResponse: AuthResponse): void {
  localStorage.setItem(TOKEN_KEY, authResponse.token);
  localStorage.setItem(REFRESH_TOKEN_KEY, authResponse.refreshToken);
  localStorage.setItem(USER_KEY, JSON.stringify(authResponse.user));
  
  const expiryTime = Date.now() + (authResponse.expiresIn * 1000);
  localStorage.setItem(TOKEN_EXPIRY_KEY, expiryTime.toString());
}

Token Expiration

Tokens are considered expired 5 minutes before actual expiration:
private readonly TOKEN_EXPIRY_BUFFER = 5 * 60 * 1000; // 5 minutes

isTokenExpired(): boolean {
  const expiryTime = parseInt(localStorage.getItem(TOKEN_EXPIRY_KEY), 10);
  const now = Date.now();
  
  return now >= (expiryTime - this.TOKEN_EXPIRY_BUFFER);
}

Reactive State

Signals

The AuthService uses Angular signals for reactive state:
// Authentication state
isAuthenticated = signal<boolean>(false);
currentUser = signal<UserInfo | null>(null);
isLoading = signal<boolean>(false);

// Pending login (for role selection)
pendingLoginResponse = signal<{ 
  loginResponse: LoginResponse; 
  authResponse: AuthResponse 
} | null>(null);
Usage in Components:
export class HeaderComponent {
  authService = inject(AuthService);
  
  // Computed values
  isLoggedIn = computed(() => this.authService.isAuthenticated());
  userName = computed(() => this.authService.currentUser()?.fullname);
}

Mock Authentication

For development without a backend, enable mock authentication:
// environment.ts
export const environment = {
  auth: {
    useMockAuth: true  // Set to false for real API
  }
};
Mock authentication:
  • Simulates API delay (500ms)
  • Creates mock users based on username
  • Supports multi-role users (username: ‘elgrandeahc’)
  • Generates mock JWT tokens

Auth Guard

Location: app/guards/auth.guard.ts:1 Protects routes from unauthorized access:
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.checkAuth()) {
    return true;
  }
  
  router.navigate(['/auth/login'], { 
    queryParams: { returnUrl: state.url } 
  });
  return false;
};
Usage:
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    canActivate: [authGuard]
  }
];

Best Practices

1. Use Signals for Reactive State

export class MyComponent {
  authService = inject(AuthService);
  
  // Reactive computed values
  isAdmin = computed(() => 
    this.authService.currentUser()?.selectedRole === RolesEnum.SYSTEM_ADMIN
  );
}

2. Check Auth on App Initialization

constructor() {
  this.checkStoredSession();
}

private checkStoredSession(): void {
  if (this.checkAuth()) {
    // Session restored
  }
}

3. Handle Multi-Role Users

this.authService.login(credentials).subscribe({
  next: (response) => {
    // Single role - logged in successfully
  },
  error: (error) => {
    if (error.needsRoleSelection) {
      // Multiple roles - show selection
      this.showRoleSelector(error.roleDetails);
    } else {
      // Actual error
      this.handleError(error);
    }
  }
});

4. Sync Auth State

The AuthService synchronizes state with UsersService:
private handleAuthSuccess(response: AuthResponse): void {
  this.sessionService.saveSession(response);
  this.isAuthenticated.set(true);
  this.currentUser.set(response.user);
  
  // Sync with UsersService
  this.usersService.setCurrentUser(response.user);
}

Security Considerations

Token Storage

  • Tokens are stored in localStorage (not cookies)
  • Sensitive headers are redacted in logs
  • Tokens are cleared on logout and error

Token Refresh

  • Automatic refresh on 401 errors
  • 5-minute expiration buffer
  • Logout on refresh failure

Session Validation

  • Token expiration checked on every auth check
  • Invalid sessions automatically cleared
  • User redirected to login on expired session

Build docs developers (and LLMs) love